tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from datetime import datetime, timedelta
  41from dateutil.tz import tzlocal, tzutc
  42from time import sleep
  43
  44import re
  45import json
  46import requests
  47import traceback as tb
  48from typing import Union
  49
  50from multiprocessing import cpu_count
  51from multiprocessing.pool import ThreadPool
  52import pandas as pd
  53
  54from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  55from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  56
  57from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  58from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  59
  60import UniLogger as uLog  # Logger for TKSBrokerAPI
  61
  62
  63# --- Common technical parameters:
  64
  65PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  66uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  67uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  68uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  69
  70__version__ = "1.5"  # The "major.minor" version setup here, but build number define at the build-server only
  71
  72CPU_COUNT = cpu_count()  # host's real CPU count
  73CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  74
  75
  76def GetDatesAsString(start: str = None, end: str = None) -> tuple:
  77    """
  78    Create tuple of date and time strings with timezone parsed from user-friendly date.
  79
  80    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
  81
  82    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
  83    An error exception will occur if input date has incorrect format.
  84
  85    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
  86    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
  87    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
  88    Start day may be negative integer numbers: `-1`, `-2`, `-3` — how many days ago.
  89
  90    Also, you can use keywords for start if `end=None`:
  91    `today` (from 00:00:00 to the end of current day),
  92    `yesterday` (-1 day from 00:00:00 to 23:59:59),
  93    `week` (-7 day from 00:00:00 to the end of current day),
  94    `month` (-30 day from 00:00:00 to the end of current day),
  95    `year` (-365 day from 00:00:00 to the end of current day),
  96
  97    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
  98             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
  99             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
 100    """
 101    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
 102    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
 103    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
 104
 105    # time between start and the end of the current day:
 106    if start is None or start.lower() == "today":
 107        pass
 108
 109    # from start of the last day to the end of the last day:
 110    elif start.lower() == "yesterday":
 111        s -= timedelta(days=1)
 112        e -= timedelta(days=1)
 113
 114    # week (-7 day from 00:00:00 to the end of the current day):
 115    elif start.lower() == "week":
 116        s -= timedelta(days=6)  # +1 current day already taken into account
 117
 118    # month (-30 day from 00:00:00 to the end of current day):
 119    elif start.lower() == "month":
 120        s -= timedelta(days=29)  # +1 current day already taken into account
 121
 122    # year (-365 day from 00:00:00 to the end of current day):
 123    elif start.lower() == "year":
 124        s -= timedelta(days=364)  # +1 current day already taken into account
 125
 126    # -N days ago to the end of current day:
 127    elif start.startswith('-') and start[1:].isdigit():
 128        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
 129
 130    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
 131    else:
 132        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
 133        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
 134
 135    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
 136    s = s.strftime(TKS_DATE_TIME_FORMAT)
 137    e = e.strftime(TKS_DATE_TIME_FORMAT)
 138
 139    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
 140
 141    return s, e
 142
 143
 144class TinkoffBrokerServer:
 145    """
 146    This class implements methods to work with Tinkoff broker server.
 147
 148    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 149
 150    About `token`: https://tinkoff.github.io/investAPI/token/
 151    """
 152    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 153        """
 154        Main class init.
 155
 156        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 157        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 158                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 159        :param useCache: use default cache file with raw data to use instead of `iList`.
 160                         True by default. Cache is auto-update if new day has come.
 161                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 162        :param defaultCache: path to default cache file. `dump.json` by default.
 163        """
 164        if token is None or not token:
 165            try:
 166                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 167                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 168
 169            except KeyError:
 170                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 171                raise Exception("Token required")
 172
 173        else:
 174            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 175            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 176
 177        if accountId is None or not accountId:
 178            try:
 179                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 180                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 181
 182            except KeyError:
 183                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 184
 185        else:
 186            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 187            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 188
 189        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 190        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 191
 192        Latest version: https://pypi.org/project/tksbrokerapi/
 193        """
 194
 195        self.aliases = TKS_TICKER_ALIASES
 196        """Some aliases instead official tickers.
 197
 198        See also: `TKSEnums.TKS_TICKER_ALIASES`
 199        """
 200
 201        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 202
 203        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 204
 205        self.ticker = ""
 206        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 207
 208        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 209        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 210
 211        See also: `SearchByTicker()`, `SearchInstruments()`.
 212        """
 213
 214        self.figi = ""
 215        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 216
 217        See also: `SearchByFIGI()`, `SearchInstruments()`.
 218        """
 219
 220        self.depth = 1
 221        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 222
 223        See also: `GetCurrentPrices()`.
 224        """
 225
 226        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 227        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 228
 229        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 230        """
 231
 232        uLogger.debug("Broker API server: {}".format(self.server))
 233
 234        self.timeout = 15
 235        """Server operations timeout in seconds. Default: `15`.
 236
 237        See also: `SendAPIRequest()`.
 238        """
 239
 240        self.headers = {
 241            "Content-Type": "application/json",
 242            "accept": "application/json",
 243            "Authorization": "Bearer {}".format(self.token),
 244            "x-app-name": "Tim55667757.TKSBrokerAPI",
 245        }
 246        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 247
 248        See also: `SendAPIRequest()`.
 249        """
 250
 251        self.body = None
 252        """Request body which send to broker server. Default: `None`.
 253
 254        See also: `SendAPIRequest()`.
 255        """
 256
 257        self.moreDebug = False
 258        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 259
 260        self.historyFile = None
 261        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 262
 263        See also: `History()`.
 264        """
 265
 266        self.htmlHistoryFile = "index.html"
 267        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 268
 269        See also: `ShowHistoryChart()`.
 270        """
 271
 272        self.instrumentsFile = "instruments.md"
 273        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 274
 275        See also: `ShowInstrumentsInfo()`.
 276        """
 277
 278        self.searchResultsFile = "search-results.md"
 279        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 280
 281        See also: `SearchInstruments()`.
 282        """
 283
 284        self.pricesFile = "prices.md"
 285        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 286
 287        See also: `GetListOfPrices()`.
 288        """
 289
 290        self.infoFile = "info.md"
 291        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 292
 293        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 294        """
 295
 296        self.bondsXLSXFile = "ext-bonds.xlsx"
 297        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 298        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 299
 300        See also: `ExtendBondsData()`.
 301        """
 302
 303        self.calendarFile = "calendar.md"
 304        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 305        
 306        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 307
 308        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 309        """
 310
 311        self.overviewFile = "overview.md"
 312        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 313
 314        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 315        """
 316
 317        self.overviewDigestFile = "overview-digest.md"
 318        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 319
 320        See also: `Overview()` with parameter `details="digest"`.
 321        """
 322
 323        self.overviewPositionsFile = "overview-positions.md"
 324        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 325
 326        See also: `Overview()` with parameter `details="positions"`.
 327        """
 328
 329        self.overviewOrdersFile = "overview-orders.md"
 330        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 331
 332        See also: `Overview()` with parameter `details="orders"`.
 333        """
 334
 335        self.overviewAnalyticsFile = "overview-analytics.md"
 336        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 337
 338        See also: `Overview()` with parameter `details="analytics"`.
 339        """
 340
 341        self.overviewBondsCalendarFile = "overview-calendar.md"
 342        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 343
 344        See also: `Overview()` with parameter `details="calendar"`.
 345        """
 346
 347        self.reportFile = "deals.md"
 348        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 349
 350        See also: `Deals()`.
 351        """
 352
 353        self.withdrawalLimitsFile = "limits.md"
 354        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 355
 356        See also: `OverviewLimits()` and `RequestLimits()`.
 357        """
 358
 359        self.userInfoFile = "user-info.md"
 360        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 361
 362        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 363        """
 364
 365        self.userAccountsFile = "accounts.md"
 366        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 367
 368        See also: `OverviewAccounts()`, `RequestAccounts()`.
 369        """
 370
 371        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 372        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 373
 374        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 375
 376        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 377        """
 378
 379        self.iList = None  # init iList for raw instruments data
 380        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 381        
 382        See also: `Listing()`, `DumpInstruments()`.
 383        """
 384
 385        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 386        if useCache:
 387            if os.path.exists(self.iListDumpFile):
 388                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 389                curTime = datetime.now(tzutc())
 390
 391                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 392                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 393
 394                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 395
 396                else:
 397                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 398
 399                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 400                        os.path.abspath(self.iListDumpFile),
 401                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 402                    ))
 403
 404            else:
 405                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 406                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 407
 408        else:
 409            self.iList = self.Listing()  # request new raw instruments data from broker server
 410            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 411
 412        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 413        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 414
 415        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 416        """
 417
 418    def _ParseJSON(self, rawData="{}") -> dict:
 419        """
 420        Parse JSON from response string.
 421
 422        :param rawData: this is a string with JSON-formatted text.
 423        :return: JSON (dictionary), parsed from server response string.
 424        """
 425        responseJSON = json.loads(rawData) if rawData else {}
 426
 427        if self.moreDebug:
 428            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 429
 430        return responseJSON
 431
 432    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 433        """
 434        Send GET or POST request to broker server and receive JSON object.
 435
 436        self.header: must be defining with dictionary of headers.
 437        self.body: if define then used as request body. None by default.
 438        self.timeout: global request timeout, 15 seconds by default.
 439        :param url: url with REST request.
 440        :param reqType: send "GET" or "POST" request. "GET" by default.
 441        :param retry: how many times retry after first request if an 5xx server errors occurred.
 442        :param pause: sleep time in seconds between retries.
 443        :return: response JSON (dictionary) from broker.
 444        """
 445        if reqType not in ("GET", "POST"):
 446            uLogger.error("You can define request type: 'GET' or 'POST'!")
 447            raise Exception("Incorrect value")
 448
 449        if self.moreDebug:
 450            uLogger.debug("Request parameters:")
 451            uLogger.debug("    - REST API URL: {}".format(url))
 452            uLogger.debug("    - request type: {}".format(reqType))
 453            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 454            uLogger.debug("    - body:\n{}".format(self.body))
 455
 456        # fast hack to avoid all operations with some tickers/FIGI
 457        responseJSON = {}
 458        oK = True
 459        for item in self.exclude:
 460            if item in url:
 461                if self.moreDebug:
 462                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 463
 464                oK = False
 465                break
 466
 467        if oK:
 468            counter = 0
 469            response = None
 470            errMsg = ""
 471
 472            while not response and counter <= retry:
 473                if reqType == "GET":
 474                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 475
 476                if reqType == "POST":
 477                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 478
 479                if self.moreDebug:
 480                    uLogger.debug("Response:")
 481                    uLogger.debug("    - status code: {}".format(response.status_code))
 482                    uLogger.debug("    - reason: {}".format(response.reason))
 483                    uLogger.debug("    - body length: {}".format(len(response.text)))
 484                    uLogger.debug("    - headers:\n{}".format(response.headers))
 485
 486                # Server returns some headers:
 487                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 488                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 489                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 490                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 491                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 492                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 493                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 494                    sleep(rateLimitWait)
 495
 496                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 497                if 400 <= response.status_code < 500:
 498                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 499                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 500                    counter = retry + 1
 501
 502                if 500 <= response.status_code < 600:
 503                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 504                    uLogger.debug("    - not oK, {}".format(errMsg))
 505                    counter += 1
 506
 507                    if counter <= retry:
 508                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 509                        sleep(pause)
 510
 511            responseJSON = self._ParseJSON(rawData=response.text)
 512
 513            if errMsg:
 514                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 515                uLogger.error("    - not oK, {}".format(errMsg))
 516
 517        return responseJSON
 518
 519    def _IUpdater(self, iType: str) -> tuple:
 520        """
 521        Request instrument by type from server. See available API methods for instruments:
 522        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 523        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 524        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 525        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 526        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 527
 528        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 529        :return: tuple with iType name and list of available instruments of current type for defined user token.
 530        """
 531        result = []
 532
 533        if iType in TKS_INSTRUMENTS:
 534            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 535
 536            # all instruments have the same body in API v2 requests:
 537            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 538            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 539            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 540
 541        return iType, result
 542
 543    def _IWrapper(self, kwargs):
 544        """
 545        Wrapper runs instrument's update method `_IUpdater()`.
 546        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 547        """
 548        return self._IUpdater(**kwargs)
 549
 550    def Listing(self) -> dict:
 551        """
 552        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 553
 554        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 555        """
 556        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 557        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 558
 559        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 560        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 561        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 562
 563        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 564        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 565        poolUpdater.close()
 566
 567        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 568        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 569        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 570
 571        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 572        for iType in iList.keys():
 573            for ticker in iList[iType]:
 574                iList[iType][ticker]["type"] = iType
 575
 576                if "minPriceIncrement" in iList[iType][ticker].keys():
 577                    iList[iType][ticker]["step"] = NanoToFloat(
 578                        iList[iType][ticker]["minPriceIncrement"]["units"],
 579                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 580                    )
 581
 582                else:
 583                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 584
 585        return iList
 586
 587    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 588        """
 589        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 590
 591        See also: `DumpInstruments()`, `Listing()`.
 592
 593        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 594                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 595        """
 596        if self.iListDumpFile is None or not self.iListDumpFile:
 597            uLogger.error("Output name of dump file must be defined!")
 598            raise Exception("Filename required")
 599
 600        if not self.iList or forceUpdate:
 601            self.iList = self.Listing()
 602
 603        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 604
 605        # Save as XLSX with separated sheets for every type of instruments:
 606        with pd.ExcelWriter(
 607                path=xlsxDumpFile,
 608                date_format=TKS_DATE_FORMAT,
 609                datetime_format=TKS_DATE_TIME_FORMAT,
 610                mode="w",
 611        ) as writer:
 612            for iType in TKS_INSTRUMENTS:
 613                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 614                df = df[sorted(df)]  # sorted by column names
 615                df = df.applymap(
 616                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 617                    na_action="ignore",
 618                )  # converting numbers from nano-type to float in every cell
 619                df.to_excel(
 620                    writer,
 621                    sheet_name=iType,
 622                    encoding="UTF-8",
 623                    freeze_panes=(1, 1),
 624                )  # saving as XLSX-file with freeze first row and column as headers
 625
 626        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 627
 628    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 629        """
 630        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 631        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 632
 633        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 634
 635        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 636                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 637        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 638        """
 639        if self.iListDumpFile is None or not self.iListDumpFile:
 640            uLogger.error("Output name of dump file must be defined!")
 641            raise Exception("Filename required")
 642
 643        if not self.iList or forceUpdate:
 644            self.iList = self.Listing()
 645
 646        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 647        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 648            fH.write(jsonDump)
 649
 650        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 651
 652        return jsonDump
 653
 654    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 655        """
 656        Show information about one instrument defined by json data and prints it in Markdown format.
 657
 658        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 659
 660        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 661        :param show: if `True` then also printing information about instrument and its current price.
 662        :return: multilines text in Markdown format with information about one instrument.
 663        """
 664        splitLine = "|                                                             |                                                        |\n"
 665        infoText = ""
 666
 667        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 668            info = [
 669                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 670                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 671                "| Parameters                                                  | Values                                                 |\n",
 672                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 673                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 674                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 675            ]
 676
 677            if "sector" in iJSON.keys() and iJSON["sector"]:
 678                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 679
 680            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 681                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 682
 683            info.extend([
 684                splitLine,
 685                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 686                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 687            ])
 688
 689            if "isin" in iJSON.keys() and iJSON["isin"]:
 690                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 691
 692            if "classCode" in iJSON.keys():
 693                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 694
 695            info.extend([
 696                splitLine,
 697                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 698                splitLine,
 699                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 700                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 701                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 702            ])
 703
 704            if iJSON["figi"]:
 705                self.figi = iJSON["figi"]
 706                iJSON = iJSON | self.RequestTradingStatus()
 707
 708                info.extend([
 709                    splitLine,
 710                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 711                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 712                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 713                ])
 714
 715            info.append(splitLine)
 716
 717            if "type" in iJSON.keys() and iJSON["type"]:
 718                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 719
 720                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 721                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 722
 723            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 724                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 725
 726            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 727                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 728
 729            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 730                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 731
 732            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 733                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 734
 735            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 736                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 737
 738            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 739                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 740
 741            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 742                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 743
 744            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 745                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 746
 747            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 748                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 749
 750            if "currency" in iJSON.keys():
 751                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 752
 753            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 754                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 755
 756            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 757                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 758
 759            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 760                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 761
 762            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 763                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 764
 765            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 766                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 767
 768            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 769                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 770
 771            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 772                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 773
 774            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 775                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 776
 777            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 778                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 779
 780            iExt = None
 781            if iJSON["type"] == "Bonds":
 782                info.extend([
 783                    splitLine,
 784                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 785                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 786                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 787                        iJSON["nominal"]["currency"],
 788                    )),
 789                ])
 790
 791                if "floatingCouponFlag" in iJSON.keys():
 792                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 793
 794                if "amortizationFlag" in iJSON.keys():
 795                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 796
 797                info.append(splitLine)
 798
 799                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 800                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 801
 802                if iJSON["figi"]:
 803                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 804
 805                    info.extend([
 806                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 807                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 808                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 809                    ])
 810
 811                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 812                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 813                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 814                        iJSON["aciValue"]["currency"]
 815                    )))
 816
 817            if "currentPrice" in iJSON.keys():
 818                info.append(splitLine)
 819
 820                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 821                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 822
 823                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 824                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 825                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 826                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 827                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 828
 829                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 830                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 831
 832                info.extend([
 833                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 834                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 835                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 836                    )),
 837                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 838                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 839                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 840                    )),
 841                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 842                        "{:.2f}%{}".format(
 843                            iJSON["currentPrice"]["changes"],
 844                            " ({}{:.2f} {})".format(
 845                                "+" if bondChangesDelta > 0 else "",
 846                                bondChangesDelta,
 847                                aciCurrency
 848                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 849                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 850                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 851                                currency
 852                            ),
 853                        )
 854                    ),
 855                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 856                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 857                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 858                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 859                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 860                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 861                    )),
 862                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 863                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 864                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 865                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 866                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 867                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 868                    )),
 869                ])
 870
 871            if "lot" in iJSON.keys():
 872                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 873
 874            if "step" in iJSON.keys() and iJSON["step"] != 0:
 875                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 876
 877            # Add bond payment calendar:
 878            if iJSON["type"] == "Bonds":
 879                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 880                info.extend(["\n", strCalendar])
 881
 882            infoText += "".join(info)
 883
 884            if show:
 885                uLogger.info("{}".format(infoText))
 886
 887            else:
 888                uLogger.debug("{}".format(infoText))
 889
 890            if self.infoFile is not None:
 891                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 892                    fH.write(infoText)
 893
 894                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 895
 896        return infoText
 897
 898    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 899        """
 900        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 901
 902        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 903        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 904        :return: JSON formatted data with information about instrument.
 905        """
 906        tickerJSON = {}
 907        if self.moreDebug:
 908            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 909
 910        if not self.ticker:
 911            uLogger.warning("self.ticker variable is not be empty!")
 912
 913        else:
 914            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 915                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 916                raise Exception("Instrument not allowed")
 917
 918            if not self.iList:
 919                self.iList = self.Listing()
 920
 921            if self.ticker in self.iList["Shares"].keys():
 922                tickerJSON = self.iList["Shares"][self.ticker]
 923                if self.moreDebug:
 924                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 925
 926            elif self.ticker in self.iList["Currencies"].keys():
 927                tickerJSON = self.iList["Currencies"][self.ticker]
 928                if self.moreDebug:
 929                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 930
 931            elif self.ticker in self.iList["Bonds"].keys():
 932                tickerJSON = self.iList["Bonds"][self.ticker]
 933                if self.moreDebug:
 934                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 935
 936            elif self.ticker in self.iList["Etfs"].keys():
 937                tickerJSON = self.iList["Etfs"][self.ticker]
 938                if self.moreDebug:
 939                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 940
 941            elif self.ticker in self.iList["Futures"].keys():
 942                tickerJSON = self.iList["Futures"][self.ticker]
 943                if self.moreDebug:
 944                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 945
 946        if tickerJSON:
 947            self.figi = tickerJSON["figi"]
 948
 949            if requestPrice:
 950                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 951
 952                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 953                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 954
 955                else:
 956                    tickerJSON["currentPrice"]["changes"] = 0
 957
 958            if show:
 959                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 960
 961        else:
 962            if show:
 963                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
 964
 965        return tickerJSON
 966
 967    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 968        """
 969        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 970
 971        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 972        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 973        :return: JSON formatted data with information about instrument.
 974        """
 975        figiJSON = {}
 976        if self.moreDebug:
 977            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
 978
 979        if not self.figi:
 980            uLogger.warning("self.figi variable is not be empty!")
 981
 982        else:
 983            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 984                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
 985                raise Exception("Instrument not allowed")
 986
 987            if not self.iList:
 988                self.iList = self.Listing()
 989
 990            for item in self.iList["Shares"].keys():
 991                if self.figi == self.iList["Shares"][item]["figi"]:
 992                    figiJSON = self.iList["Shares"][item]
 993
 994                    if self.moreDebug:
 995                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
 996
 997                    break
 998
 999            if not figiJSON:
1000                for item in self.iList["Currencies"].keys():
1001                    if self.figi == self.iList["Currencies"][item]["figi"]:
1002                        figiJSON = self.iList["Currencies"][item]
1003
1004                        if self.moreDebug:
1005                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1006
1007                        break
1008
1009            if not figiJSON:
1010                for item in self.iList["Bonds"].keys():
1011                    if self.figi == self.iList["Bonds"][item]["figi"]:
1012                        figiJSON = self.iList["Bonds"][item]
1013
1014                        if self.moreDebug:
1015                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1016
1017                        break
1018
1019            if not figiJSON:
1020                for item in self.iList["Etfs"].keys():
1021                    if self.figi == self.iList["Etfs"][item]["figi"]:
1022                        figiJSON = self.iList["Etfs"][item]
1023
1024                        if self.moreDebug:
1025                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1026
1027                        break
1028
1029            if not figiJSON:
1030                for item in self.iList["Futures"].keys():
1031                    if self.figi == self.iList["Futures"][item]["figi"]:
1032                        figiJSON = self.iList["Futures"][item]
1033
1034                        if self.moreDebug:
1035                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1036
1037                        break
1038
1039        if figiJSON:
1040            self.figi = figiJSON["figi"]
1041            self.ticker = figiJSON["ticker"]
1042
1043            if requestPrice:
1044                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1045
1046                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1047                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1048
1049                else:
1050                    figiJSON["currentPrice"]["changes"] = 0
1051
1052            if show:
1053                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1054
1055        else:
1056            if show:
1057                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1058
1059        return figiJSON
1060
1061    def GetCurrentPrices(self, show: bool = True) -> dict:
1062        """
1063        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1064        `{"buy": [{"price": 1243.8, "quantity": 193},
1065                  {"price": 1244.0, "quantity": 168},
1066                  {"price": 1244.8, "quantity": 5},
1067                  {"price": 1245.0, "quantity": 61},
1068                  {"price": 1245.4, "quantity": 60}],
1069          "sell": [{"price": 1243.6, "quantity": 8},
1070                   {"price": 1242.6, "quantity": 10},
1071                   {"price": 1242.4, "quantity": 18},
1072                   {"price": 1242.2, "quantity": 50},
1073                   {"price": 1242.0, "quantity": 113}],
1074          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1075        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1076        - sell: list of dicts with Buyers prices,
1077            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1078            - quantity: volume value by current price in lots,
1079        - limitUp: current trade session limit price, maximum,
1080        - limitDown: current trade session limit price, minimum,
1081        - lastPrice: last deal price of the instrument,
1082        - closePrice: previous trade session close price of the instrument.
1083
1084        See also: `SearchByTicker()` and `SearchByFIGI()`.
1085        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1086        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1087
1088        :param show: if `True` then print DOM to log and console.
1089        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1090                 If an error occurred then returns an empty record:
1091                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1092        """
1093        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1094
1095        if self.depth < 1:
1096            uLogger.error("Depth of Market (DOM) must be >=1!")
1097            raise Exception("Incorrect value")
1098
1099        if not (self.ticker or self.figi):
1100            uLogger.error("self.ticker or self.figi variables must be defined!")
1101            raise Exception("Ticker or FIGI required")
1102
1103        if self.ticker and not self.figi:
1104            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1105            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1106
1107        if not self.ticker and self.figi:
1108            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1109            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1110
1111        if not self.figi:
1112            uLogger.error("FIGI is not defined!")
1113            raise Exception("Ticker or FIGI required")
1114
1115        else:
1116            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1117
1118            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1119            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1120            self.body = str({"figi": self.figi, "depth": self.depth})
1121            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1122
1123            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1124                # list of dicts with sellers orders:
1125                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1126
1127                # list of dicts with buyers orders:
1128                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1129
1130                # max price of instrument at this time:
1131                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1132
1133                # min price of instrument at this time:
1134                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1135
1136                # last price of deal with instrument:
1137                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1138
1139                # last close price of instrument:
1140                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1141
1142            else:
1143                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1144                uLogger.debug("Server response: {}".format(pricesResponse))
1145
1146            if show:
1147                if prices["buy"] or prices["sell"]:
1148                    info = [
1149                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1150                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1151                            self.ticker,
1152                            self.figi,
1153                            self.depth,
1154                        ),
1155                        "-" * 60, "\n",
1156                        "             Orders of Buyers | Orders of Sellers\n",
1157                        "-" * 60, "\n",
1158                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1159                        "-" * 60, "\n",
1160                    ]
1161
1162                    if not prices["buy"]:
1163                        info.append("                              | No orders!\n")
1164                        sumBuy = 0
1165
1166                    else:
1167                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1168                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1169                        for item in maxMinSorted:
1170                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1171
1172                    if not prices["sell"]:
1173                        info.append("No orders!                    |\n")
1174                        sumSell = 0
1175
1176                    else:
1177                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1178                        for item in prices["sell"]:
1179                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1180
1181                    info.extend([
1182                        "-" * 60, "\n",
1183                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1184                        "-" * 60, "\n",
1185                    ])
1186
1187                    infoText = "".join(info)
1188
1189                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1190
1191                else:
1192                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1193
1194        return prices
1195
1196    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1197        """
1198        This method get and show information about all available broker instruments for current user account.
1199        If `instrumentsFile` string is not empty then also save information to this file.
1200
1201        :param show: if `True` then print results to console, if `False` — print only to file.
1202        :return: multi-lines string with all available broker instruments
1203        """
1204        if not self.iList:
1205            self.iList = self.Listing()
1206
1207        info = [
1208            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1209            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1210        ]
1211
1212        # add instruments count by type:
1213        for iType in self.iList.keys():
1214            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1215
1216        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1217        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1218
1219        # generating info tables with all instruments by type:
1220        for iType in self.iList.keys():
1221            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1222
1223            for instrument in self.iList[iType].keys():
1224                iName = self.iList[iType][instrument]["name"]  # instrument's name
1225                if len(iName) > 57:
1226                    iName = "{}...".format(iName[:54])  # right trim for a long string
1227
1228                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1229                    self.iList[iType][instrument]["ticker"],
1230                    iName,
1231                    self.iList[iType][instrument]["figi"],
1232                    self.iList[iType][instrument]["currency"],
1233                    self.iList[iType][instrument]["lot"],
1234                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1235                ))
1236
1237        infoText = "".join(info)
1238
1239        if show:
1240            uLogger.info(infoText)
1241
1242        if self.instrumentsFile:
1243            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1244                fH.write(infoText)
1245
1246            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1247
1248        return infoText
1249
1250    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1251        """
1252        This method search and show information about instruments by part of its ticker, FIGI or name.
1253        If `searchResultsFile` string is not empty then also save information to this file.
1254
1255        :param pattern: string with part of ticker, FIGI or instrument's name.
1256        :param show: if `True` then print results to console, if `False` — return list of result only.
1257        :return: list of dictionaries with all found instruments.
1258        """
1259        if not self.iList:
1260            self.iList = self.Listing()
1261
1262        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1263        compiledPattern = re.compile(pattern, re.IGNORECASE)
1264
1265        for iType in self.iList:
1266            for instrument in self.iList[iType].values():
1267                searchResult = compiledPattern.search(" ".join(
1268                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1269                ))
1270
1271                if searchResult:
1272                    searchResults[iType][instrument["ticker"]] = instrument
1273
1274        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1275        info = [
1276            "# Search results\n\n",
1277            "* **Search pattern:** [{}]\n".format(pattern),
1278            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1279            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1280        ]
1281        infoShort = info[:]
1282
1283        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1284        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1285        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1286
1287        if resultsLen == 0:
1288            info.append("\nNo results\n")
1289            infoShort.append("\nNo results\n")
1290            uLogger.warning("No results. Try changing your search pattern.")
1291
1292        else:
1293            for iType in searchResults:
1294                iTypeValuesCount = len(searchResults[iType].values())
1295                if iTypeValuesCount > 0:
1296                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1297                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1298
1299                    for instrument in searchResults[iType].values():
1300                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1301                            instrument["type"],
1302                            instrument["ticker"],
1303                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1304                            instrument["figi"],
1305                        ))
1306
1307                    if iTypeValuesCount <= 5:
1308                        infoShort.extend(info[-iTypeValuesCount:])
1309
1310                    else:
1311                        infoShort.extend(info[-5:])
1312                        infoShort.append(skippedLine)
1313
1314        infoText = "".join(info)
1315        infoTextShort = "".join(infoShort)
1316
1317        if show:
1318            uLogger.info(infoTextShort)
1319            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1320
1321        if self.searchResultsFile:
1322            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1323                fH.write(infoText)
1324
1325            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1326
1327        return searchResults
1328
1329    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1330        """
1331        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1332
1333        :param instruments: list of strings with tickers or FIGIs.
1334        :return: list with unique instrument FIGIs only.
1335        """
1336        requestedInstruments = []
1337        for iName in instruments:
1338            if iName not in self.aliases.keys():
1339                if iName not in requestedInstruments:
1340                    requestedInstruments.append(iName)
1341
1342            else:
1343                if iName not in requestedInstruments:
1344                    if self.aliases[iName] not in requestedInstruments:
1345                        requestedInstruments.append(self.aliases[iName])
1346
1347        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1348
1349        onlyUniqueFIGIs = []
1350        for iName in requestedInstruments:
1351            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1352                continue
1353
1354            self.ticker = iName
1355            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1356
1357            if not iData:
1358                self.ticker = ""
1359                self.figi = iName
1360
1361                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1362
1363                if not iData:
1364                    self.figi = ""
1365                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1366
1367            if iData and iData["figi"] not in onlyUniqueFIGIs:
1368                onlyUniqueFIGIs.append(iData["figi"])
1369
1370        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1371
1372        return onlyUniqueFIGIs
1373
1374    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1375        """
1376        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1377
1378        See limits: https://tinkoff.github.io/investAPI/limits/
1379
1380        If `pricesFile` string is not empty then also save information to this file.
1381
1382        :param instruments: list of strings with tickers or FIGIs.
1383        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1384        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1385                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1386        """
1387        if instruments is None or not instruments:
1388            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1389            raise Exception("Ticker or FIGI required")
1390
1391        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1392
1393        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1394
1395        iList = []  # trying to get info and current prices about all unique instruments:
1396        for self.figi in onlyUniqueFIGIs:
1397            iData = self.SearchByFIGI(requestPrice=True)
1398            iList.append(iData)
1399
1400        self.ShowListOfPrices(iList, show)
1401
1402        return iList
1403
1404    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1405        """
1406        Show table contains current prices of given instruments.
1407
1408        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1409                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1410        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1411        :return: multilines text in Markdown format as a table contains current prices.
1412        """
1413        infoText = ""
1414
1415        if show or self.pricesFile:
1416            info = [
1417                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1418                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1419                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1420            ]
1421
1422            for item in iList:
1423                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1424                    item["ticker"],
1425                    item["figi"],
1426                    item["type"],
1427                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1428                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1429                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1430                    "{} / {}".format(
1431                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1432                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1433                    ),
1434                    "{} / {}".format(
1435                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1436                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1437                    ),
1438                    item["currency"],
1439                ))
1440
1441            infoText = "".join(info)
1442
1443            if show:
1444                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1445
1446            if self.pricesFile:
1447                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1448                    fH.write(infoText)
1449
1450                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1451
1452        return infoText
1453
1454    def RequestTradingStatus(self) -> dict:
1455        """
1456        Requesting trading status for the instrument defined by `figi` variable.
1457
1458        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1459
1460        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1461
1462        :return: dictionary with trading status attributes. Response example:
1463                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1464                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1465        """
1466        if self.figi is None or not self.figi:
1467            uLogger.error("Variable `figi` must be defined for using this method!")
1468            raise Exception("FIGI required")
1469
1470        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1471
1472        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1473        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1474        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1475
1476        if self.moreDebug:
1477            uLogger.debug("Records about current trading status successfully received")
1478
1479        return tradingStatus
1480
1481    def RequestPortfolio(self) -> dict:
1482        """
1483        Requesting actual user's portfolio for current `accountId`.
1484
1485        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1486
1487        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1488
1489        :return: dictionary with user's portfolio.
1490        """
1491        if self.accountId is None or not self.accountId:
1492            uLogger.error("Variable `accountId` must be defined for using this method!")
1493            raise Exception("Account ID required")
1494
1495        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1496
1497        self.body = str({"accountId": self.accountId})
1498        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1499        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1500
1501        if self.moreDebug:
1502            uLogger.debug("Records about user's portfolio successfully received")
1503
1504        return rawPortfolio
1505
1506    def RequestPositions(self) -> dict:
1507        """
1508        Requesting open positions by currencies and instruments for current `accountId`.
1509
1510        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1511
1512        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1513
1514        :return: dictionary with open positions by instruments.
1515        """
1516        if self.accountId is None or not self.accountId:
1517            uLogger.error("Variable `accountId` must be defined for using this method!")
1518            raise Exception("Account ID required")
1519
1520        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1521
1522        self.body = str({"accountId": self.accountId})
1523        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1524        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1525
1526        if self.moreDebug:
1527            uLogger.debug("Records about current open positions successfully received")
1528
1529        return rawPositions
1530
1531    def RequestPendingOrders(self) -> list:
1532        """
1533        Requesting current actual pending orders for current `accountId`.
1534
1535        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1536
1537        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1538
1539        :return: list of dictionaries with pending orders.
1540        """
1541        if self.accountId is None or not self.accountId:
1542            uLogger.error("Variable `accountId` must be defined for using this method!")
1543            raise Exception("Account ID required")
1544
1545        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1546
1547        self.body = str({"accountId": self.accountId})
1548        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1549        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1550
1551        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1552
1553        return rawOrders
1554
1555    def RequestStopOrders(self) -> list:
1556        """
1557        Requesting current actual stop orders for current `accountId`.
1558
1559        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1560
1561        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1562
1563        :return: list of dictionaries with stop orders.
1564        """
1565        if self.accountId is None or not self.accountId:
1566            uLogger.error("Variable `accountId` must be defined for using this method!")
1567            raise Exception("Account ID required")
1568
1569        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1570
1571        self.body = str({"accountId": self.accountId})
1572        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1573        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1574
1575        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1576
1577        return rawStopOrders
1578
1579    def Overview(self, show: bool = False, details: str = "full") -> dict:
1580        """
1581        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1582        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1583        and `overviewBondsCalendarFile` are defined then also save information to file.
1584
1585        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1586        many requests about the state of the portfolio, and then, based on the received data, a large number
1587        of calculation and statistics are collected.
1588
1589        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1590        :param details: how detailed should the information be?
1591        - `full` — shows full available information about portfolio status (by default),
1592        - `positions` — shows only open positions,
1593        - `orders` — shows only sections of open limits and stop orders.
1594        - `digest` — show a short digest of the portfolio status,
1595        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1596        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1597        :return: dictionary with client's raw portfolio and some statistics.
1598        """
1599        if self.accountId is None or not self.accountId:
1600            uLogger.error("Variable `accountId` must be defined for using this method!")
1601            raise Exception("Account ID required")
1602
1603        view = {
1604            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1605                "headers": {},  # list of dictionaries, response headers without "positions" section
1606                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1607                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1608                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1609                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1610                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1611                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1612                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1613                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1614                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1615            },
1616            "stat": {  # --- some statistics calculated using "raw" sections:
1617                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1618                "availableRUB": 0.,  # available rubles (without other currencies)
1619                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1620                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1621                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1622                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1623                "sharesCostRUB": 0.,  # costs of all shares in RUB
1624                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1625                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1626                "futuresCostRUB": 0.,  # costs of all futures in RUB
1627                "Currencies": [],  # list of dictionaries of all currencies statistics
1628                "Shares": [],  # list of dictionaries of all shares statistics
1629                "Bonds": [],  # list of dictionaries of all bonds statistics
1630                "Etfs": [],  # list of dictionaries of all etfs statistics
1631                "Futures": [],  # list of dictionaries of all futures statistics
1632                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1633                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1634                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1635                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1636                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1637            },
1638            "analytics": {  # --- some analytics of portfolio:
1639                "distrByAssets": {},  # portfolio distribution by assets
1640                "distrByCompanies": {},  # portfolio distribution by companies
1641                "distrBySectors": {},  # portfolio distribution by sectors
1642                "distrByCurrencies": {},  # portfolio distribution by currencies
1643                "distrByCountries": {},  # portfolio distribution by countries
1644                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1645            }
1646        }
1647
1648        details = details.lower()
1649        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1650        if details not in availableDetails:
1651            details = "full"
1652            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1653
1654        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1655
1656        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1657        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1658        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1659        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1660
1661        # save response headers without "positions" section:
1662        for key in portfolioResponse.keys():
1663            if key != "positions":
1664                view["raw"]["headers"][key] = portfolioResponse[key]
1665
1666            else:
1667                continue
1668
1669        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1670        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1671        for item in portfolioResponse["positions"]:
1672            if item["instrumentType"] == "currency":
1673                self.figi = item["figi"]
1674                curr = self.SearchByFIGI(requestPrice=False)
1675
1676                # current price of currency in RUB:
1677                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1678                    "name": curr["name"],
1679                    "currentPrice": NanoToFloat(
1680                        item["currentPrice"]["units"],
1681                        item["currentPrice"]["nano"]
1682                    ),
1683                }
1684
1685                view["raw"]["Currencies"].append(item)
1686
1687            elif item["instrumentType"] == "share":
1688                view["raw"]["Shares"].append(item)
1689
1690            elif item["instrumentType"] == "bond":
1691                view["raw"]["Bonds"].append(item)
1692
1693            elif item["instrumentType"] == "etf":
1694                view["raw"]["Etfs"].append(item)
1695
1696            elif item["instrumentType"] == "futures":
1697                view["raw"]["Futures"].append(item)
1698
1699            else:
1700                continue
1701
1702        # how many volume of currencies (by ISO currency name) are blocked:
1703        for item in view["raw"]["positions"]["blocked"]:
1704            blocked = NanoToFloat(item["units"], item["nano"])
1705            if blocked > 0:
1706                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1707
1708        # how many volume of instruments (by FIGI) are blocked:
1709        for item in view["raw"]["positions"]["securities"]:
1710            blocked = int(item["blocked"])
1711            if blocked > 0:
1712                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1713
1714        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1715
1716        if "rub" in allBlocked.keys():
1717            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1718
1719        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1720        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1721        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1722        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1723        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1724        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1725        view["stat"]["portfolioCostRUB"] = sum([
1726            view["stat"]["allCurrenciesCostRUB"],
1727            view["stat"]["sharesCostRUB"],
1728            view["stat"]["bondsCostRUB"],
1729            view["stat"]["etfsCostRUB"],
1730            view["stat"]["futuresCostRUB"],
1731        ])
1732
1733        # --- calculating some portfolio statistics:
1734        byComp = {}  # distribution by companies
1735        bySect = {}  # distribution by sectors
1736        byCurr = {}  # distribution by currencies (include RUB)
1737        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1738        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1739
1740        for item in portfolioResponse["positions"]:
1741            self.figi = item["figi"]
1742            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1743
1744            if instrument:
1745                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1746                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1747
1748                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1749                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1750
1751                else:
1752                    blocked = 0
1753
1754                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1755                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1756                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1757                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1758                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1759                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1760                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1761                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1762                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1763                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1764                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1765                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1766
1767                statData = {
1768                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1769                    "ticker": instrument["ticker"],  # ticker by FIGI
1770                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1771                    "volume": volume,  # available volume of instrument
1772                    "lots": lots,  # volume in lots of instrument
1773                    "direction": direction,  # direction of an instrument's position: short or long
1774                    "blocked": blocked,  # blocked volume of currency or instrument
1775                    "currentPrice": curPrice,  # current instrument's price in basic asset
1776                    "average": average,  # current average position price
1777                    "cost": cost,  # current cost of all volume of instrument in basic asset
1778                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1779                    "costRUB": costRUB,  # cost of instrument in ruble
1780                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1781                    "profit": profit,  # expected profit at current moment
1782                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1783                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1784                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1785                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1786                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1787                    "step": instrument["step"],  # minimum price increment
1788                }
1789
1790                # adding distribution by unique countries:
1791                if statData["country"] not in byCountry.keys():
1792                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1793
1794                else:
1795                    byCountry[statData["country"]]["cost"] += costRUB
1796                    byCountry[statData["country"]]["percent"] += percentCostRUB
1797
1798                if item["instrumentType"] != "currency":
1799                    # adding distribution by unique companies:
1800                    if statData["name"]:
1801                        if statData["name"] not in byComp.keys():
1802                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1803
1804                        else:
1805                            byComp[statData["name"]]["cost"] += costRUB
1806                            byComp[statData["name"]]["percent"] += percentCostRUB
1807
1808                    # adding distribution by unique sectors:
1809                    if statData["sector"] not in bySect.keys():
1810                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1811
1812                    else:
1813                        bySect[statData["sector"]]["cost"] += costRUB
1814                        bySect[statData["sector"]]["percent"] += percentCostRUB
1815
1816                # adding distribution by unique currencies:
1817                if currency not in byCurr.keys():
1818                    byCurr[currency] = {
1819                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1820                        "cost": costRUB,
1821                        "percent": percentCostRUB
1822                    }
1823
1824                else:
1825                    byCurr[currency]["cost"] += costRUB
1826                    byCurr[currency]["percent"] += percentCostRUB
1827
1828                # saving statistics for every instrument:
1829                if item["instrumentType"] == "currency":
1830                    view["stat"]["Currencies"].append(statData)
1831
1832                    # update dict with free funds for trading (total - blocked) by currencies
1833                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1834                    view["stat"]["funds"][currency] = {
1835                        "total": volume,
1836                        "totalCostRUB": costRUB,  # total volume cost in rubles
1837                        "free": volume - blocked,
1838                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1839                    }
1840
1841                elif item["instrumentType"] == "share":
1842                    view["stat"]["Shares"].append(statData)
1843
1844                elif item["instrumentType"] == "bond":
1845                    view["stat"]["Bonds"].append(statData)
1846
1847                elif item["instrumentType"] == "etf":
1848                    view["stat"]["Etfs"].append(statData)
1849
1850                elif item["instrumentType"] == "Futures":
1851                    view["stat"]["Futures"].append(statData)
1852
1853                else:
1854                    continue
1855
1856        # total changes in Russian Ruble:
1857        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1858        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1859        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1860        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1861        view["stat"]["funds"]["rub"] = {
1862            "total": view["stat"]["availableRUB"],
1863            "totalCostRUB": view["stat"]["availableRUB"],
1864            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1865            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1866        }
1867
1868        # --- pending orders sector data:
1869        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1870        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1871
1872        for item in view["raw"]["orders"]:
1873            self.figi = item["figi"]
1874
1875            if item["figi"] not in uniquePendingOrdersFIGIs:
1876                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1877
1878                uniquePendingOrdersFIGIs.append(item["figi"])
1879                uniquePendingOrders[item["figi"]] = instrument
1880
1881            else:
1882                instrument = uniquePendingOrders[item["figi"]]
1883
1884            if instrument:
1885                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1886                orderType = TKS_ORDER_TYPES[item["orderType"]]
1887                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1888                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1889
1890                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1891                if item["direction"] == "ORDER_DIRECTION_BUY":
1892                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1893
1894                else:
1895                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1896
1897                # requested price for order execution:
1898                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1899
1900                # necessary changes in percent to reach target from current price:
1901                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1902
1903                view["stat"]["orders"].append({
1904                    "orderID": item["orderId"],  # orderId number parameter of current order
1905                    "figi": item["figi"],  # FIGI identification
1906                    "ticker": instrument["ticker"],  # ticker name by FIGI
1907                    "lotsRequested": item["lotsRequested"],  # requested lots value
1908                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1909                    "currentPrice": lastPrice,  # current instrument's price for defined action
1910                    "targetPrice": target,  # requested price for order execution in base currency
1911                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1912                    "percentChanges": changes,  # changes in percent to target from current price
1913                    "currency": item["currency"],  # instrument's currency name
1914                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1915                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1916                    "status": orderState,  # order status from TKS_ORDER_STATES
1917                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1918                })
1919
1920        # --- stop orders sector data:
1921        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1922        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1923
1924        for item in view["raw"]["stopOrders"]:
1925            self.figi = item["figi"]
1926
1927            if item["figi"] not in uniqueStopOrdersFIGIs:
1928                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1929
1930                uniqueStopOrdersFIGIs.append(item["figi"])
1931                uniqueStopOrders[item["figi"]] = instrument
1932
1933            else:
1934                instrument = uniqueStopOrders[item["figi"]]
1935
1936            if instrument:
1937                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1938                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1939                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1940
1941                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1942                if "expirationTime" in item.keys():
1943                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1944                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1945
1946                else:
1947                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1948                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1949
1950                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1951                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1952                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1953
1954                else:
1955                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1956
1957                # requested price when stop-order executed:
1958                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1959
1960                # price for limit-order, set up when stop-order executed:
1961                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1962
1963                # necessary changes in percent to reach target from current price:
1964                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1965
1966                view["stat"]["stopOrders"].append({
1967                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1968                    "figi": item["figi"],  # FIGI identification
1969                    "ticker": instrument["ticker"],  # ticker name by FIGI
1970                    "lotsRequested": item["lotsRequested"],  # requested lots value
1971                    "currentPrice": lastPrice,  # current instrument's price for defined action
1972                    "targetPrice": target,  # requested price for stop-order execution in base currency
1973                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1974                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1975                    "percentChanges": changes,  # changes in percent to target from current price
1976                    "currency": item["currency"],  # instrument's currency name
1977                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1978                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1979                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1980                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1981                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1982                })
1983
1984        # --- calculating data for analytics section:
1985        # portfolio distribution by assets:
1986        view["analytics"]["distrByAssets"] = {
1987            "Ruble": {
1988                "uniques": 1,
1989                "cost": view["stat"]["availableRUB"],
1990                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1991            },
1992            "Currencies": {
1993                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1994                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1995                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1996            },
1997            "Shares": {
1998                "uniques": len(view["stat"]["Shares"]),
1999                "cost": view["stat"]["sharesCostRUB"],
2000                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2001            },
2002            "Bonds": {
2003                "uniques": len(view["stat"]["Bonds"]),
2004                "cost": view["stat"]["bondsCostRUB"],
2005                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2006            },
2007            "Etfs": {
2008                "uniques": len(view["stat"]["Etfs"]),
2009                "cost": view["stat"]["etfsCostRUB"],
2010                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2011            },
2012            "Futures": {
2013                "uniques": len(view["stat"]["Futures"]),
2014                "cost": view["stat"]["futuresCostRUB"],
2015                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2016            },
2017        }
2018
2019        # portfolio distribution by companies:
2020        view["analytics"]["distrByCompanies"]["All money cash"] = {
2021            "ticker": "",
2022            "cost": view["stat"]["allCurrenciesCostRUB"],
2023            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2024        }
2025        view["analytics"]["distrByCompanies"].update(byComp)
2026
2027        # portfolio distribution by sectors:
2028        view["analytics"]["distrBySectors"]["All money cash"] = {
2029            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2030            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2031        }
2032        view["analytics"]["distrBySectors"].update(bySect)
2033
2034        # portfolio distribution by currencies:
2035        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2036            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2037
2038            if self.moreDebug:
2039                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2040
2041        view["analytics"]["distrByCurrencies"].update(byCurr)
2042        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2043        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2044
2045        # portfolio distribution by countries:
2046        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2047            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2048
2049            if self.moreDebug:
2050                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2051
2052        view["analytics"]["distrByCountries"].update(byCountry)
2053        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2054        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2055
2056        # --- Prepare text statistics overview in human-readable:
2057        if show:
2058            # Whatever the value `details`, header not changes:
2059            info = [
2060                "# Client's portfolio\n\n",
2061                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2062                "* **Account ID:** [{}]\n".format(self.accountId),
2063            ]
2064
2065            if details in ["full", "positions", "digest"]:
2066                info.extend([
2067                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2068                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2069                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2070                        view["stat"]["totalChangesRUB"],
2071                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2072                        view["stat"]["totalChangesPercentRUB"],
2073                    ),
2074                ])
2075
2076            if details in ["full", "positions"]:
2077                info.extend([
2078                    "## Open positions\n\n",
2079                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2080                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2081                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2082                        "{:.2f} ({:.2f}) rub".format(
2083                            view["stat"]["availableRUB"],
2084                            view["stat"]["blockedRUB"],
2085                        )
2086                    )
2087                ])
2088
2089                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2090                    return [
2091                        "|                             |                                 |          |              |              |                     |                              |\n",
2092                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2093                            noTradeStr if noTradeStr else typeStr,
2094                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2095                        ),
2096                    ]
2097
2098                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2099                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2100                        "{} [{}]".format(data["ticker"], data["figi"]),
2101                        "{:.2f} ({:.2f}) {}".format(
2102                            data["volume"],
2103                            data["blocked"],
2104                            data["currency"],
2105                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2106                            data["volume"],
2107                            data["blocked"],
2108                        ),
2109                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2110                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2111                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2112                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2113                        "{}{:.2f} {} ({}{:.2f}%)".format(
2114                            "+" if data["profit"] > 0 else "",
2115                            data["profit"], data["baseCurrencyName"],
2116                            "+" if data["percentProfit"] > 0 else "",
2117                            data["percentProfit"],
2118                        ),
2119                    )
2120
2121                # --- Show currencies section:
2122                if view["stat"]["Currencies"]:
2123                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2124                    for item in view["stat"]["Currencies"]:
2125                        info.append(_InfoStr(item, showCurrencyName=True))
2126
2127                else:
2128                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2129
2130                # --- Show shares section:
2131                if view["stat"]["Shares"]:
2132                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2133
2134                    for item in view["stat"]["Shares"]:
2135                        info.append(_InfoStr(item))
2136
2137                else:
2138                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2139
2140                # --- Show bonds section:
2141                if view["stat"]["Bonds"]:
2142                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2143
2144                    for item in view["stat"]["Bonds"]:
2145                        info.append(_InfoStr(item))
2146
2147                else:
2148                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2149
2150                # --- Show etfs section:
2151                if view["stat"]["Etfs"]:
2152                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2153
2154                    for item in view["stat"]["Etfs"]:
2155                        info.append(_InfoStr(item))
2156
2157                else:
2158                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2159
2160                # --- Show futures section:
2161                if view["stat"]["Futures"]:
2162                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2163
2164                    for item in view["stat"]["Futures"]:
2165                        info.append(_InfoStr(item))
2166
2167                else:
2168                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2169
2170            if details in ["full", "orders"]:
2171                # --- Show pending orders section:
2172                if view["stat"]["orders"]:
2173                    info.extend([
2174                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2175                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2176                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2177                    ])
2178
2179                    for item in view["stat"]["orders"]:
2180                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2181                            "{} [{}]".format(item["ticker"], item["figi"]),
2182                            item["orderID"],
2183                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2184                            "{} {} ({}{:.2f}%)".format(
2185                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2186                                item["baseCurrencyName"],
2187                                "+" if item["percentChanges"] > 0 else "",
2188                                float(item["percentChanges"]),
2189                            ),
2190                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2191                            item["action"],
2192                            item["type"],
2193                            item["date"],
2194                        ))
2195
2196                else:
2197                    info.append("\n## Total pending limit-orders: 0\n")
2198
2199                # --- Show stop orders section:
2200                if view["stat"]["stopOrders"]:
2201                    info.extend([
2202                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2203                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2204                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2205                    ])
2206
2207                    for item in view["stat"]["stopOrders"]:
2208                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2209                            "{} [{}]".format(item["ticker"], item["figi"]),
2210                            item["orderID"],
2211                            item["lotsRequested"],
2212                            "{} {} ({}{:.2f}%)".format(
2213                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2214                                item["baseCurrencyName"],
2215                                "+" if item["percentChanges"] > 0 else "",
2216                                float(item["percentChanges"]),
2217                            ),
2218                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2219                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2220                            item["action"],
2221                            item["type"],
2222                            item["expType"],
2223                            item["createDate"],
2224                            item["expDate"],
2225                        ))
2226
2227                else:
2228                    info.append("\n## Total stop-orders: 0\n")
2229
2230            if details in ["full", "analytics"]:
2231                # -- Show analytics section:
2232                if view["stat"]["portfolioCostRUB"] > 0:
2233                    info.extend([
2234                        "\n# Analytics\n"
2235                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2236                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2237                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2238                            view["stat"]["totalChangesRUB"],
2239                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2240                            view["stat"]["totalChangesPercentRUB"],
2241                        ),
2242                        "\n## Portfolio distribution by assets\n"
2243                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2244                        "|------------------------------------|---------|---------|--------------------|\n",
2245                    ])
2246
2247                    for key in view["analytics"]["distrByAssets"].keys():
2248                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2249                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2250                                key,
2251                                view["analytics"]["distrByAssets"][key]["uniques"],
2252                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2253                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2254                            ))
2255
2256                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2257
2258                    info.extend([
2259                        "\n## Portfolio distribution by companies\n"
2260                        "\n| Company                                      | Percent | Current cost       |\n",
2261                        aSepLine,
2262                    ])
2263
2264                    for company in view["analytics"]["distrByCompanies"].keys():
2265                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2266                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2267                                "{}{}".format(
2268                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2269                                    company,
2270                                ),
2271                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2272                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2273                            ))
2274
2275                    info.extend([
2276                        "\n## Portfolio distribution by sectors\n"
2277                        "\n| Sector                                       | Percent | Current cost       |\n",
2278                        aSepLine,
2279                    ])
2280
2281                    for sector in view["analytics"]["distrBySectors"].keys():
2282                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2283                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2284                                sector,
2285                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2286                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2287                            ))
2288
2289                    info.extend([
2290                        "\n## Portfolio distribution by currencies\n"
2291                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2292                        aSepLine,
2293                    ])
2294
2295                    for curr in view["analytics"]["distrByCurrencies"].keys():
2296                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2297                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2298                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2299                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2300                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2301                            ))
2302
2303                    info.extend([
2304                        "\n## Portfolio distribution by countries\n"
2305                        "\n| Assets by country                            | Percent | Current cost       |\n",
2306                        aSepLine,
2307                    ])
2308
2309                    for country in view["analytics"]["distrByCountries"].keys():
2310                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2311                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2312                                country,
2313                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2314                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2315                            ))
2316
2317            if details in ["full", "calendar"]:
2318                # -- Show bonds payment calendar section:
2319                if view["stat"]["Bonds"]:
2320                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2321                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2322                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2323
2324                else:
2325                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2326
2327            infoText = "".join(info)
2328
2329            uLogger.info(infoText)
2330
2331            if details == "full" and self.overviewFile:
2332                filename = self.overviewFile
2333
2334            elif details == "digest" and self.overviewDigestFile:
2335                filename = self.overviewDigestFile
2336
2337            elif details == "positions" and self.overviewPositionsFile:
2338                filename = self.overviewPositionsFile
2339
2340            elif details == "orders" and self.overviewOrdersFile:
2341                filename = self.overviewOrdersFile
2342
2343            elif details == "analytics" and self.overviewAnalyticsFile:
2344                filename = self.overviewAnalyticsFile
2345
2346            elif details == "calendar" and self.overviewBondsCalendarFile:
2347                filename = self.overviewBondsCalendarFile
2348
2349            else:
2350                filename = ""
2351
2352            if filename:
2353                with open(filename, "w", encoding="UTF-8") as fH:
2354                    fH.write(infoText)
2355
2356                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2357
2358        return view
2359
2360    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2361        """
2362        Returns history operations between two given dates for current `accountId`.
2363        If `reportFile` string is not empty then also save human-readable report.
2364        Shows some statistical data of closed positions.
2365
2366        :param start: see docstring in `GetDatesAsString()` method
2367        :param end: see docstring in `GetDatesAsString()` method
2368        :param show: if `True` then also prints all records to the console.
2369        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2370        :return: original list of dictionaries with history of deals records from API ("operations" key):
2371                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2372                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2373        """
2374        if self.accountId is None or not self.accountId:
2375            uLogger.error("Variable `accountId` must be defined for using this method!")
2376            raise Exception("Account ID required")
2377
2378        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2379
2380        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2381
2382        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2383        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2384        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2385        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2386        customStat = {}  # custom statistics in additional to responseJSON
2387
2388        # --- output report in human-readable format:
2389        if show or self.reportFile:
2390            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2391            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2392            nextDay = ""
2393
2394            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2395
2396            if len(ops) > 0:
2397                customStat = {
2398                    "opsCount": 0,  # total operations count
2399                    "buyCount": 0,  # buy operations
2400                    "sellCount": 0,  # sell operations
2401                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2402                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2403                    "payIn": {"rub": 0.},  # Deposit brokerage account
2404                    "payOut": {"rub": 0.},  # Withdrawals
2405                    "divs": {"rub": 0.},  # Dividends income
2406                    "coupons": {"rub": 0.},  # Coupon's income
2407                    "brokerCom": {"rub": 0.},  # Service commissions
2408                    "serviceCom": {"rub": 0.},  # Service commissions
2409                    "marginCom": {"rub": 0.},  # Margin commissions
2410                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2411                }
2412
2413                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2414                for item in ops:
2415                    if item["state"] == "OPERATION_STATE_EXECUTED":
2416                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2417
2418                        # count buy operations:
2419                        if "_BUY" in item["operationType"]:
2420                            customStat["buyCount"] += 1
2421
2422                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2423                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2424
2425                            else:
2426                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2427
2428                        # count sell operations:
2429                        elif "_SELL" in item["operationType"]:
2430                            customStat["sellCount"] += 1
2431
2432                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2433                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2434
2435                            else:
2436                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2437
2438                        # count incoming operations:
2439                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2440                            if item["payment"]["currency"] in customStat["payIn"].keys():
2441                                customStat["payIn"][item["payment"]["currency"]] += payment
2442
2443                            else:
2444                                customStat["payIn"][item["payment"]["currency"]] = payment
2445
2446                        # count withdrawals operations:
2447                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2448                            if item["payment"]["currency"] in customStat["payOut"].keys():
2449                                customStat["payOut"][item["payment"]["currency"]] += payment
2450
2451                            else:
2452                                customStat["payOut"][item["payment"]["currency"]] = payment
2453
2454                        # count dividends income:
2455                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2456                            if item["payment"]["currency"] in customStat["divs"].keys():
2457                                customStat["divs"][item["payment"]["currency"]] += payment
2458
2459                            else:
2460                                customStat["divs"][item["payment"]["currency"]] = payment
2461
2462                        # count coupon's income:
2463                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2464                            if item["payment"]["currency"] in customStat["coupons"].keys():
2465                                customStat["coupons"][item["payment"]["currency"]] += payment
2466
2467                            else:
2468                                customStat["coupons"][item["payment"]["currency"]] = payment
2469
2470                        # count broker commissions:
2471                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2472                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2473                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2474
2475                            else:
2476                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2477
2478                        # count service commissions:
2479                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2480                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2481                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2482
2483                            else:
2484                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2485
2486                        # count margin commissions:
2487                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2488                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2489                                customStat["marginCom"][item["payment"]["currency"]] += payment
2490
2491                            else:
2492                                customStat["marginCom"][item["payment"]["currency"]] = payment
2493
2494                        # count withholding taxes:
2495                        elif "_TAX" in item["operationType"]:
2496                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2497                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2498
2499                            else:
2500                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2501
2502                        else:
2503                            continue
2504
2505                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2506
2507                # --- view "Actions" lines:
2508                info.extend([
2509                    "| Report sections            |                               |                              |                      |                        |\n",
2510                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2511                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2512                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2513                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2514                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2515                    ),
2516                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2517                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2518                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2519                    ),
2520                ])
2521
2522                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2523                for key in opsKeys:
2524                    if key == "rub":
2525                        continue
2526
2527                    info.extend([
2528                        "|                            |                               | {:<28} |                      |                        |\n".format(
2529                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2530                        ),
2531                        "|                            |                               | {:<28} |                      |                        |\n".format(
2532                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2533                        ),
2534                    ])
2535
2536                info.append(splitLine1)
2537
2538                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2539                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2540                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2541                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2542                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2543                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2544                    )
2545
2546                # --- view "Payments" lines:
2547                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2548                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2549
2550                for key in paymentsKeys:
2551                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2552
2553                info.append(splitLine1)
2554
2555                # --- view "Commissions and taxes" lines:
2556                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2557                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2558
2559                for key in comKeys:
2560                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2561
2562                info.append(splitLine1)
2563
2564                info.extend([
2565                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2566                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2567                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2568                ])
2569
2570            else:
2571                info.append("Broker returned no operations during this period\n")
2572
2573            # --- view "Operations" section:
2574            for item in ops:
2575                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2576                    continue
2577
2578                else:
2579                    self.figi = item["figi"] if item["figi"] else ""
2580                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2581                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2582
2583                    # group of deals during one day:
2584                    if nextDay and item["date"].split("T")[0] != nextDay:
2585                        info.append(splitLine2)
2586                        nextDay = ""
2587
2588                    else:
2589                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2590
2591                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2592                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2593                        self.figi if self.figi else "—",
2594                        instrument["ticker"] if instrument else "—",
2595                        instrument["type"] if instrument else "—",
2596                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2597                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2598                        TKS_OPERATION_STATES[item["state"]],
2599                        TKS_OPERATION_TYPES[item["operationType"]],
2600                    ))
2601
2602            infoText = "".join(info)
2603
2604            if show:
2605                if self.moreDebug:
2606                    uLogger.debug("Records about history of a client's operations successfully received")
2607
2608                uLogger.info(infoText)
2609
2610            if self.reportFile:
2611                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2612                    fH.write(infoText)
2613
2614                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2615
2616        return ops, customStat
2617
2618    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2619        """
2620        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2621
2622        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2623        Warning! Broker server used ISO UTC time by default.
2624
2625        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2626        Also, `historyFile` used to update history with `onlyMissing` parameter.
2627
2628        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2629
2630        :param start: see docstring in `GetDatesAsString()` method.
2631        :param end: see docstring in `GetDatesAsString()` method.
2632        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2633                         `"hour"`, `"day"`. Default: `"hour"`.
2634        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2635                            False by default. Warning! History appends only from last candle to current time
2636                            with always update last candle!
2637        :param csvSep: separator if csv-file is used, `,` by default.
2638        :param show: if `True` then also prints Pandas DataFrame to the console.
2639        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2640                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2641        """
2642        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2643        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2644        history = None  # empty pandas object for history
2645
2646        if interval not in TKS_CANDLE_INTERVALS.keys():
2647            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2648            raise Exception("Incorrect value")
2649
2650        if not (self.ticker or self.figi):
2651            uLogger.error("Ticker or FIGI must be defined!")
2652            raise Exception("Ticker or FIGI required")
2653
2654        if self.ticker and not self.figi:
2655            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2656            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2657
2658        if self.figi and not self.ticker:
2659            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2660            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2661
2662        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2663        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2664        if interval.lower() != "day":
2665            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2666
2667        delta = dtEnd - dtStart  # current UTC time minus last time in file
2668        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2669
2670        # calculate history length in candles:
2671        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2672        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2673            length += 1  # to avoid fraction time
2674
2675        # calculate data blocks count:
2676        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2677
2678        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2679        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2680        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2681        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2682        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2683
2684        tempOld = None  # pandas object for old history, if --only-missing key present
2685        lastTime = None  # datetime object of last old candle in file
2686
2687        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2688            uLogger.debug("--only-missing key present, add only last missing candles...")
2689            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2690
2691            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2692
2693            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2694            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2695            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2696            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2697
2698            # get last datetime object from last string in file or minus 1 delta if file is empty:
2699            if len(tempOld) > 0:
2700                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2701
2702            else:
2703                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2704
2705            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2706
2707        responseJSONs = []  # raw history blocks of data
2708
2709        blockEnd = dtEnd
2710        for item in range(blocks):
2711            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2712            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2713
2714            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2715                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2716            ))
2717
2718            if blockStart == blockEnd:
2719                uLogger.debug("Skipped this zero-length block...")
2720
2721            else:
2722                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2723                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2724                self.body = str({
2725                    "figi": self.figi,
2726                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2727                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2728                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2729                })
2730                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2731
2732                if "code" in responseJSON.keys():
2733                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2734
2735                else:
2736                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2737                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2738
2739                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2740
2741            blockEnd = blockStart
2742
2743        printCount = len(responseJSONs)  # candles to show in console
2744        if responseJSONs:
2745            tempHistory = pd.DataFrame(
2746                data={
2747                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2748                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2749                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2750                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2751                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2752                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2753                    "volume": [int(item["volume"]) for item in responseJSONs],
2754                },
2755                index=range(len(responseJSONs)),
2756                columns=["date", "time", "open", "high", "low", "close", "volume"],
2757            )
2758            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2759            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2760
2761            # append only newest candles to old history if --only-missing key present:
2762            if onlyMissing and tempOld is not None and lastTime is not None:
2763                index = 0  # find start index in tempHistory data:
2764
2765                for i, item in tempHistory.iterrows():
2766                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2767
2768                    if curTime == lastTime:
2769                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2770                        index = i
2771                        printCount = index + 1
2772                        break
2773
2774                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2775
2776            else:
2777                history = tempHistory  # if no `--only-missing` key then load full data from server
2778
2779            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2780
2781        if history is not None and not history.empty:
2782            if show:
2783                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2784                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2785                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2786                ))
2787
2788        else:
2789            uLogger.warning("Received an empty candles history!")
2790
2791        if self.historyFile is not None:
2792            if history is not None and not history.empty:
2793                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2794                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2795
2796            else:
2797                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2798
2799        else:
2800            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2801
2802        return history
2803
2804    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2805        """
2806        Load candles history from csv-file and return Pandas DataFrame object.
2807
2808        See also: `History()` and `ShowHistoryChart()` methods.
2809
2810        :param filePath: path to csv-file to open.
2811        """
2812        loadedHistory = None  # init candles data object
2813
2814        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2815
2816        if os.path.exists(filePath):
2817            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2818
2819            tfStr = self.priceModel.FormattedDelta(
2820                self.priceModel.timeframe,
2821                "{days} days {hours}h {minutes}m {seconds}s",
2822            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2823                self.priceModel.timeframe,
2824                "{hours}h {minutes}m {seconds}s",
2825            )
2826
2827            if loadedHistory is not None and not loadedHistory.empty:
2828                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2829                    len(loadedHistory),
2830                    tfStr,
2831                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2832                )
2833
2834            else:
2835                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2836
2837        else:
2838            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2839
2840        return loadedHistory
2841
2842    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2843        """
2844        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2845
2846        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2847        Default: `index.html` (both for interact and non-interact candlesticks chart).
2848
2849        See also: `History()` and `LoadHistory()` methods.
2850
2851        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2852        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2853                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2854                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2855                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2856        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2857                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2858        """
2859        if isinstance(candles, str):
2860            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2861            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2862
2863        elif isinstance(candles, pd.DataFrame):
2864            self.priceModel.prices = candles  # set candles chain from variable
2865            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2866
2867            if "datetime" not in candles.columns:
2868                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2869
2870        else:
2871            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2872            raise Exception("Incorrect value")
2873
2874        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2875
2876        if interact:
2877            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2878
2879            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2880
2881        else:
2882            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2883
2884            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2885
2886        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2887
2888    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2889        """
2890        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2891        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2892
2893        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2894
2895        :param operation: string "Buy" or "Sell".
2896        :param lots: volume, integer count of lots >= 1.
2897        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2898        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2899        :param expDate: string "Undefined" by default or local date in future,
2900                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2901        :return: JSON with response from broker server.
2902        """
2903        if self.accountId is None or not self.accountId:
2904            uLogger.error("Variable `accountId` must be defined for using this method!")
2905            raise Exception("Account ID required")
2906
2907        if operation is None or not operation or operation not in ("Buy", "Sell"):
2908            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2909            raise Exception("Incorrect value")
2910
2911        if lots is None or lots < 1:
2912            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2913            lots = 1
2914
2915        if tp is None or tp < 0:
2916            tp = 0
2917
2918        if sl is None or sl < 0:
2919            sl = 0
2920
2921        if expDate is None or not expDate:
2922            expDate = "Undefined"
2923
2924        if not (self.ticker or self.figi):
2925            uLogger.error("Ticker or FIGI must be defined!")
2926            raise Exception("Ticker or FIGI required")
2927
2928        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2929        self.ticker = instrument["ticker"]
2930        self.figi = instrument["figi"]
2931
2932        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2933
2934        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2935        self.body = str({
2936            "figi": self.figi,
2937            "quantity": str(lots),
2938            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2939            "accountId": str(self.accountId),
2940            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2941        })
2942        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2943
2944        if "orderId" in response.keys():
2945            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2946                operation, response["orderId"],
2947                self.ticker, self.figi, lots,
2948                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2949                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2950                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2951            ))
2952
2953            if tp > 0:
2954                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2955
2956            if sl > 0:
2957                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2958
2959        else:
2960            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2961
2962        return response
2963
2964    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2965        """
2966        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2967        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2968
2969        See also: `Order()` and `Trade()` docstrings.
2970
2971        :param lots: volume, integer count of lots >= 1.
2972        :param tp: float > 0, take profit price of stop-order.
2973        :param sl: float > 0, stop loss price of stop-order.
2974        :param expDate: it's a local date in future.
2975                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2976        :return: JSON with response from broker server.
2977        """
2978        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2979
2980    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2981        """
2982        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2983        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2984
2985        See also: `Order()` and `Trade()` docstrings.
2986
2987        :param lots: volume, integer count of lots >= 1.
2988        :param tp: float > 0, take profit price of stop-order.
2989        :param sl: float > 0, stop loss price of stop-order.
2990        :param expDate: it's a local date in the future.
2991                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2992        :return: JSON with response from broker server.
2993        """
2994        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2995
2996    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2997        """
2998        Close position of given instruments.
2999
3000        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3001        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3002                         This avoids unnecessary downloading data from the server.
3003        """
3004        if instruments is None or not instruments:
3005            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3006            raise Exception("Ticker or FIGI required")
3007
3008        if isinstance(instruments, str):
3009            instruments = [instruments]
3010
3011        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3012        if uniqueInstruments:
3013            if portfolio is None or not portfolio:
3014                portfolio = self.Overview(show=False)
3015
3016            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3017            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3018
3019            for self.figi in uniqueInstruments:
3020                if self.figi not in allOpened:
3021                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3022                    continue
3023
3024                # search open trade info about instrument by ticker:
3025                instrument = {}
3026                for iType in TKS_INSTRUMENTS:
3027                    if instrument:
3028                        break
3029
3030                    for item in portfolio["stat"][iType]:
3031                        if item["figi"] == self.figi:
3032                            instrument = item
3033                            break
3034
3035                if instrument:
3036                    self.ticker = instrument["ticker"]
3037                    self.figi = instrument["figi"]
3038
3039                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3040                        self.ticker,
3041                        self.figi,
3042                        int(instrument["volume"]),
3043                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3044                    ))
3045
3046                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3047
3048                    if tradeLots > 0:
3049                        if instrument["blocked"] > 0:
3050                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3051                                instrument["blocked"],
3052                                self.ticker,
3053                                tradeLots,
3054                            ))
3055
3056                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3057                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3058
3059                    else:
3060                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3061
3062    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3063        """
3064        Close all positions of given instruments with defined type.
3065
3066        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3067        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3068                         This avoids unnecessary downloading data from the server.
3069        """
3070        if iType not in TKS_INSTRUMENTS:
3071            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3072
3073        else:
3074            if portfolio is None or not portfolio:
3075                portfolio = self.Overview(show=False)
3076
3077            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3078            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3079
3080            if tickers and portfolio:
3081                self.CloseTrades(tickers, portfolio)
3082
3083            else:
3084                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3085
3086    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3087        """
3088        Universal method to create market or limit orders with all available parameters for current `accountId`.
3089        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3090
3091        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3092        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3093
3094        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3095        then broker immediately open market order as you can do simple --buy or --sell operations!
3096
3097        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3098        When current price will go up or down to target price value then broker opens a limit order.
3099        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3100
3101        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3102
3103        :param operation: string "Buy" or "Sell".
3104        :param orderType: string "Limit" or "Stop".
3105        :param lots: volume, integer count of lots >= 1.
3106        :param targetPrice: target price > 0. This is open trade price for limit order.
3107        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3108                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3109        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3110                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3111                         Stop loss order always executed by market price.
3112        :param expDate: string "Undefined" by default or local date in future.
3113                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3114                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3115                        A limit order has no expiration date, it lasts until the end of the trading day.
3116        :return: JSON with response from broker server.
3117        """
3118        if self.accountId is None or not self.accountId:
3119            uLogger.error("Variable `accountId` must be defined for using this method!")
3120            raise Exception("Account ID required")
3121
3122        if operation is None or not operation or operation not in ("Buy", "Sell"):
3123            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3124            raise Exception("Incorrect value")
3125
3126        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3127            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3128            raise Exception("Incorrect value")
3129
3130        if lots is None or lots < 1:
3131            uLogger.error("You must define trade volume > 0: integer count of lots!")
3132            raise Exception("Incorrect value")
3133
3134        if targetPrice is None or targetPrice <= 0:
3135            uLogger.error("Target price for limit-order must be greater than 0!")
3136            raise Exception("Incorrect value")
3137
3138        if limitPrice is None or limitPrice <= 0:
3139            limitPrice = targetPrice
3140
3141        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3142            stopType = "Limit"
3143
3144        if expDate is None or not expDate:
3145            expDate = "Undefined"
3146
3147        if not (self.ticker or self.figi):
3148            uLogger.error("Tocker or FIGI must be defined!")
3149            raise Exception("Ticker or FIGI required")
3150
3151        response = {}
3152        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3153        self.ticker = instrument["ticker"]
3154        self.figi = instrument["figi"]
3155
3156        if orderType == "Limit":
3157            uLogger.debug(
3158                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3159                    self.ticker, self.figi,
3160                    operation, lots, targetPrice, instrument["currency"],
3161                ))
3162
3163            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3164            self.body = str({
3165                "figi": self.figi,
3166                "quantity": str(lots),
3167                "price": FloatToNano(targetPrice),
3168                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3169                "accountId": str(self.accountId),
3170                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3171            })
3172            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3173
3174            if "orderId" in response.keys():
3175                uLogger.info(
3176                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3177                        response["orderId"],
3178                        self.ticker, self.figi,
3179                        operation, lots, targetPrice, instrument["currency"],
3180                    ))
3181
3182                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3183                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3184                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3185                            targetPrice, instrument["currency"],
3186                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3187                        ))
3188
3189                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3190                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3191                            targetPrice, instrument["currency"],
3192                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3193                        ))
3194
3195            else:
3196                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3197
3198        if orderType == "Stop":
3199            uLogger.debug(
3200                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3201                    self.ticker, self.figi,
3202                    operation, lots,
3203                    targetPrice, instrument["currency"],
3204                    limitPrice, instrument["currency"],
3205                    stopType, expDate,
3206                ))
3207
3208            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3209            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3210            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3211
3212            body = {
3213                "figi": self.figi,
3214                "quantity": str(lots),
3215                "price": FloatToNano(limitPrice),
3216                "stopPrice": FloatToNano(targetPrice),
3217                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3218                "accountId": str(self.accountId),
3219                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3220                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3221            }
3222
3223            if expDateUTC:
3224                body["expireDate"] = expDateUTC
3225
3226            self.body = str(body)
3227            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3228
3229            if "stopOrderId" in response.keys():
3230                uLogger.info(
3231                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3232                        response["stopOrderId"],
3233                        self.ticker, self.figi,
3234                        operation, lots,
3235                        targetPrice, instrument["currency"],
3236                        limitPrice, instrument["currency"],
3237                        TKS_STOP_ORDER_TYPES[stopOrderType],
3238                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3239                    ))
3240
3241                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3242                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3243                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3244                            targetPrice, instrument["currency"],
3245                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3246                        ))
3247
3248                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3249                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3250                            targetPrice, instrument["currency"],
3251                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3252                        ))
3253
3254            else:
3255                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3256
3257        return response
3258
3259    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3260        """
3261        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3262        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3263        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3264        See also: `Order()` docstring.
3265
3266        :param lots: volume, integer count of lots >= 1.
3267        :param targetPrice: target price > 0. This is open trade price for limit order.
3268        :return: JSON with response from broker server.
3269        """
3270        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3271
3272    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3273        """
3274        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3275        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3276        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3277        target price value then broker opens a limit order. See also: `Order()` docstring.
3278
3279        :param lots: volume, integer count of lots >= 1.
3280        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3281        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3282                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3283        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3284                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3285        :param expDate: string "Undefined" by default or local date in future.
3286                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3287                        This date is converting to UTC format for server.
3288        :return: JSON with response from broker server.
3289        """
3290        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3291
3292    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3293        """
3294        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3295        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3296        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3297        See also: `Order()` docstring.
3298
3299        :param lots: volume, integer count of lots >= 1.
3300        :param targetPrice: target price > 0. This is open trade price for limit order.
3301        :return: JSON with response from broker server.
3302        """
3303        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3304
3305    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3306        """
3307        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3308        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3309        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3310        target price value then broker opens a limit order. See also: `Order()` docstring.
3311
3312        :param lots: volume, integer count of lots >= 1.
3313        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3314        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3315                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3316        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3317                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3318        :param expDate: string "Undefined" by default or local date in future.
3319                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3320                        This date is converting to UTC format for server.
3321        :return: JSON with response from broker server.
3322        """
3323        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3324
3325    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3326        """
3327        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3328
3329        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3330        :param allOrdersIDs: pre-received lists of all active pending orders.
3331                             This avoids unnecessary downloading data from the server.
3332        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3333        """
3334        if self.accountId is None or not self.accountId:
3335            uLogger.error("Variable `accountId` must be defined for using this method!")
3336            raise Exception("Account ID required")
3337
3338        if orderIDs:
3339            if allOrdersIDs is None or not allOrdersIDs:
3340                rawOrders = self.RequestPendingOrders()
3341                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3342
3343            if allStopOrdersIDs is None or not allStopOrdersIDs:
3344                rawStopOrders = self.RequestStopOrders()
3345                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3346
3347            for orderID in orderIDs:
3348                idInPendingOrders = orderID in allOrdersIDs
3349                idInStopOrders = orderID in allStopOrdersIDs
3350
3351                if not (idInPendingOrders or idInStopOrders):
3352                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3353                    continue
3354
3355                else:
3356                    if idInPendingOrders:
3357                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3358
3359                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3360                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3361                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3362                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3363
3364                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3365                            if self.moreDebug:
3366                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3367
3368                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3369
3370                        else:
3371                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3372
3373                    elif idInStopOrders:
3374                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3375
3376                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3377                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3378                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3379                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3380
3381                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3382                            if self.moreDebug:
3383                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3384
3385                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3386
3387                        else:
3388                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3389
3390                    else:
3391                        continue
3392
3393    def CloseAllOrders(self) -> None:
3394        """
3395        Gets a list of open pending and stop orders and cancel it all.
3396        """
3397        rawOrders = self.RequestPendingOrders()
3398        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3399        lenOrders = len(allOrdersIDs)
3400
3401        rawStopOrders = self.RequestStopOrders()
3402        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3403        lenSOrders = len(allStopOrdersIDs)
3404
3405        if lenOrders > 0 or lenSOrders > 0:
3406            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3407
3408            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3409
3410        else:
3411            uLogger.info("Orders not found, nothing to cancel.")
3412
3413    def CloseAll(self, *args) -> None:
3414        """
3415        Close all available (not blocked) opened trades and orders.
3416
3417        Also, you can select one or more keywords case-insensitive:
3418        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3419
3420        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3421        """
3422        overview = self.Overview(show=False)  # get all open trades info
3423
3424        if len(args) == 0:
3425            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3426            self.CloseAllOrders()  # close all pending and stop orders
3427
3428            for iType in TKS_INSTRUMENTS:
3429                if iType != "Currencies":
3430                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3431
3432        else:
3433            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3434            lowerArgs = [x.lower() for x in args]
3435
3436            if "orders" in lowerArgs:
3437                self.CloseAllOrders()  # close all pending and stop orders
3438
3439            for iType in TKS_INSTRUMENTS:
3440                if iType.lower() in lowerArgs and iType != "Currencies":
3441                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3442
3443    @staticmethod
3444    def ParseOrderParameters(operation, **inputParameters):
3445        """
3446        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3447
3448        :param operation: string "Buy" or "Sell".
3449        :param inputParameters: this is dict of strings that looks like this
3450               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3451               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3452               "prices" key: one or more prices to open limit-orders
3453               Counts of values in lots and prices lists must be equals!
3454        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3455        """
3456        # TODO: update order grid work with api v2
3457        pass
3458        # uLogger.debug("Input parameters: {}".format(inputParameters))
3459        #
3460        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3461        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3462        #     raise Exception("Incorrect value")
3463        #
3464        # if "l" in inputParameters.keys():
3465        #     inputParameters["lots"] = inputParameters.pop("l")
3466        #
3467        # if "p" in inputParameters.keys():
3468        #     inputParameters["prices"] = inputParameters.pop("p")
3469        #
3470        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3471        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3472        #     raise Exception("Incorrect value")
3473        #
3474        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3475        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3476        #
3477        # if len(lots) != len(prices):
3478        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3479        #     raise Exception("Incorrect value")
3480        #
3481        # uLogger.debug("Extracted parameters for orders:")
3482        # uLogger.debug("lots = {}".format(lots))
3483        # uLogger.debug("prices = {}".format(prices))
3484        #
3485        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3486        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3487        # uLogger.debug("Order parameters: {}".format(result))
3488        #
3489        # return result
3490
3491    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3492        """
3493        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3494
3495        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3496        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3497        """
3498        result = False
3499        msg = "Instrument not defined!"
3500
3501        if portfolio is None or not portfolio:
3502            portfolio = self.Overview(show=False)
3503
3504        if self.ticker:
3505            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3506            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3507
3508            for iType in TKS_INSTRUMENTS:
3509                for instrument in portfolio["stat"][iType]:
3510                    if instrument["ticker"] == self.ticker:
3511                        result = True
3512                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3513                        break
3514
3515        elif self.figi:
3516            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3517            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3518
3519            for iType in TKS_INSTRUMENTS:
3520                for instrument in portfolio["stat"][iType]:
3521                    if instrument["figi"] == self.figi:
3522                        result = True
3523                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3524                        break
3525
3526        else:
3527            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3528
3529        uLogger.debug(msg)
3530
3531        return result
3532
3533    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3534        """
3535        Returns instrument from the user's portfolio if it presents there.
3536        Instrument must be defined by `ticker` (highly priority) or `figi`.
3537
3538        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3539        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3540        """
3541        result = None
3542        msg = "Instrument not defined!"
3543
3544        if portfolio is None or not portfolio:
3545            portfolio = self.Overview(show=False)
3546
3547        if self.ticker:
3548            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3549            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3550
3551            for iType in TKS_INSTRUMENTS:
3552                for instrument in portfolio["stat"][iType]:
3553                    if instrument["ticker"] == self.ticker:
3554                        result = instrument
3555                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3556                        break
3557
3558        elif self.figi:
3559            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3560            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3561
3562            for iType in TKS_INSTRUMENTS:
3563                for instrument in portfolio["stat"][iType]:
3564                    if instrument["figi"] == self.figi:
3565                        result = instrument
3566                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3567                        break
3568
3569        else:
3570            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3571
3572        uLogger.debug(msg)
3573
3574        return result
3575
3576    def RequestLimits(self) -> dict:
3577        """
3578        Method for obtaining the available funds for withdrawal for current `accountId`.
3579
3580        See also:
3581        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3582        - `OverviewLimits()` method
3583
3584        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3585                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3586                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3587                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3588        """
3589        if self.accountId is None or not self.accountId:
3590            uLogger.error("Variable `accountId` must be defined for using this method!")
3591            raise Exception("Account ID required")
3592
3593        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3594
3595        self.body = str({"accountId": self.accountId})
3596        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3597        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3598
3599        if self.moreDebug:
3600            uLogger.debug("Records about available funds for withdrawal successfully received")
3601
3602        return rawLimits
3603
3604    def OverviewLimits(self, show: bool = False) -> dict:
3605        """
3606        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3607
3608        See also: `RequestLimits()`.
3609
3610        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3611        :return: dict with raw parsed data from server and some calculated statistics about it.
3612        """
3613        if self.accountId is None or not self.accountId:
3614            uLogger.error("Variable `accountId` must be defined for using this method!")
3615            raise Exception("Account ID required")
3616
3617        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3618
3619        view = {
3620            "rawLimits": rawLimits,
3621            "limits": {  # parsed data for every currency:
3622                "money": {  # this is an array of portfolio currency positions
3623                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3624                },
3625                "blocked": {  # this is an array of blocked currency
3626                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3627                },
3628                "blockedGuarantee": {  # this is locked money under collateral for futures
3629                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3630                },
3631            },
3632        }
3633
3634        # --- Prepare text table with limits in human-readable format:
3635        if show:
3636            info = [
3637                "# Withdrawal limits\n\n",
3638                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3639                "* **Account ID:** [{}]\n".format(self.accountId),
3640            ]
3641
3642            if view["limits"]["money"]:
3643                info.extend([
3644                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3645                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3646                ])
3647
3648            else:
3649                info.append("\nNo withdrawal limits\n")
3650
3651            for curr in view["limits"]["money"].keys():
3652                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3653                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3654                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3655
3656                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3657                    "[{}]".format(curr),
3658                    "{:.2f}".format(view["limits"]["money"][curr]),
3659                    "{:.2f}".format(availableMoney),
3660                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3661                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3662                )
3663
3664                if curr == "rub":
3665                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3666
3667                else:
3668                    info.append(infoStr)
3669
3670            infoText = "".join(info)
3671
3672            uLogger.info(infoText)
3673
3674            if self.withdrawalLimitsFile:
3675                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3676                    fH.write(infoText)
3677
3678                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3679
3680        return view
3681
3682    def RequestAccounts(self) -> dict:
3683        """
3684        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3685
3686        See also:
3687        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3688        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3689        - `OverviewUserInfo()` method
3690
3691        :return: dict with raw data from server that contains accounts info. Example of dict:
3692                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3693                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3694                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3695                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3696        """
3697        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3698
3699        self.body = str({})
3700        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3701        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3702
3703        if self.moreDebug:
3704            uLogger.debug("Records about available accounts successfully received")
3705
3706        return rawAccounts
3707
3708    def RequestUserInfo(self) -> dict:
3709        """
3710        Method for requesting common user's information.
3711
3712        See also:
3713        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3714        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3715        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3716        - `OverviewUserInfo()` method
3717
3718        :return: dict with raw data from server that contains user's information. Example of dict:
3719                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3720                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3721        """
3722        uLogger.debug("Requesting common user's information. Wait, please...")
3723
3724        self.body = str({})
3725        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3726        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3727
3728        if self.moreDebug:
3729            uLogger.debug("Records about current user successfully received")
3730
3731        return rawUserInfo
3732
3733    def RequestMarginStatus(self, accountId: str = None) -> dict:
3734        """
3735        Method for requesting margin calculation for defined account ID.
3736
3737        See also:
3738        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3739        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3740        - `OverviewUserInfo()` method
3741
3742        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3743        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3744                 Example of responses:
3745                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3746                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3747                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3748                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3749                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3750                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3751        """
3752        if accountId is None or not accountId:
3753            if self.accountId is None or not self.accountId:
3754                uLogger.error("Variable `accountId` must be defined for using this method!")
3755                raise Exception("Account ID required")
3756
3757            else:
3758                accountId = self.accountId  # use `self.accountId` (main ID) by default
3759
3760        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3761
3762        self.body = str({"accountId": accountId})
3763        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3764        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3765
3766        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3767            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3768            rawMargin = {}
3769
3770        else:
3771            if self.moreDebug:
3772                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3773
3774        return rawMargin
3775
3776    def RequestTariffLimits(self) -> dict:
3777        """
3778        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3779
3780        See also:
3781        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3782        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3783        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3784        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3785        - `OverviewUserInfo()` method
3786
3787        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3788                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3789                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3790        """
3791        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3792
3793        self.body = str({})
3794        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3795        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3796
3797        if self.moreDebug:
3798            uLogger.debug("Records with limits of current tariff successfully received")
3799
3800        return rawTariffLimits
3801
3802    def RequestBondCoupons(self, iJSON: dict) -> dict:
3803        """
3804        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3805        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3806        All dates are in UTC timezone.
3807
3808        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3809        Documentation:
3810        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3811        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3812
3813        See also: `ExtendBondsData()`.
3814
3815        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3816                      If raw iJSON is not data of bond then server returns an error [400] with message:
3817                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3818        :return: dictionary with bond payment calendar. Response example
3819                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3820                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3821                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3822                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3823        """
3824        if iJSON["figi"] is None or not iJSON["figi"]:
3825            uLogger.error("FIGI must be defined for using this method!")
3826            raise Exception("FIGI required")
3827
3828        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3829        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3830
3831        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3832            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3833            self.figi,
3834            startDate,
3835            endDate,
3836        ))
3837
3838        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3839        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3840        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3841
3842        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3843            uLogger.warning("Instrument type is not bond!")
3844
3845        else:
3846            if self.moreDebug:
3847                uLogger.debug("Records about bond payment calendar successfully received")
3848
3849        return calendar
3850
3851    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3852        """
3853        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3854        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3855        coupon yields, current yields and some statistics etc.
3856
3857        WARNING! This is too long operation if a lot of bonds requested from broker server.
3858
3859        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3860
3861        :param instruments: list of strings with tickers or FIGIs.
3862        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3863                     for further used by data scientists or stock analytics.
3864        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3865                 In XLSX-file and Pandas DataFrame fields mean:
3866                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3867                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3868        """
3869        if instruments is None or not instruments:
3870            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3871            raise Exception("Ticker or FIGI required")
3872
3873        if isinstance(instruments, str):
3874            instruments = [instruments]
3875
3876        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3877
3878        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3879
3880        iCount = len(uniqueInstruments)
3881        tooLong = iCount >= 20
3882        if tooLong:
3883            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3884
3885        bonds = None
3886        for i, self.figi in enumerate(uniqueInstruments):
3887            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3888
3889            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3890                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3891                rawBond = self.SearchByFIGI(requestPrice=True)
3892
3893                # Widen raw data with UTC current time (iData["actualDateTime"]):
3894                actualDate = datetime.now(tzutc())
3895                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3896
3897                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3898                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3899
3900                # Replace some values with human-readable:
3901                iData["nominalCurrency"] = iData["nominal"]["currency"]
3902                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3903                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3904                iData["aciCurrency"] = iData["aciValue"]["currency"]
3905                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3906                iData["issueSize"] = int(iData["issueSize"])
3907                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3908                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3909                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3910                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3911                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3912                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3913                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3914                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3915                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3916                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3917
3918                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3919                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3920                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3921                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3922                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3923                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3924                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3925                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3926                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3927                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3928                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3929
3930                # Widen raw data with calendar data from `rawCalendar` values:
3931                calendarData = []
3932                if "events" in iData["rawCalendar"].keys():
3933                    for item in iData["rawCalendar"]["events"]:
3934                        calendarData.append({
3935                            "couponDate": item["couponDate"],
3936                            "couponNumber": int(item["couponNumber"]),
3937                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3938                            "payCurrency": item["payOneBond"]["currency"],
3939                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3940                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3941                            "couponStartDate": item["couponStartDate"],
3942                            "couponEndDate": item["couponEndDate"],
3943                            "couponPeriod": item["couponPeriod"],
3944                        })
3945
3946                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3947                    if "maturityDate" not in iData.keys():
3948                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3949
3950                # Widen raw data with Coupon Rate.
3951                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3952                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3953                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3954                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3955
3956                # Widen raw data with Yield to Maturity (YTM) on current date.
3957                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3958                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3959                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3960                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3961                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3962                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3963
3964                iData["calendar"] = calendarData  # adds calendar at the end
3965
3966                # Remove not used data:
3967                iData.pop("uid")
3968                iData.pop("positionUid")
3969                iData.pop("currentPrice")
3970                iData.pop("rawCalendar")
3971
3972                colNames = list(iData.keys())
3973                if bonds is None:
3974                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3975
3976                else:
3977                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3978
3979            else:
3980                uLogger.warning("Instrument is not a bond!")
3981
3982            processed = round(100 * (i + 1) / iCount, 1)
3983            if tooLong and processed % 5 == 0:
3984                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3985
3986            else:
3987                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3988
3989        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3990
3991        # Saving bonds from Pandas DataFrame to XLSX sheet:
3992        if xlsx and self.bondsXLSXFile:
3993            with pd.ExcelWriter(
3994                    path=self.bondsXLSXFile,
3995                    date_format=TKS_DATE_FORMAT,
3996                    datetime_format=TKS_DATE_TIME_FORMAT,
3997                    mode="w",
3998            ) as writer:
3999                bonds.to_excel(
4000                    writer,
4001                    sheet_name="Extended bonds data",
4002                    index=True,
4003                    encoding="UTF-8",
4004                    freeze_panes=(1, 1),
4005                )  # saving as XLSX-file with freeze first row and column as headers
4006
4007            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4008
4009        return bonds
4010
4011    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4012        """
4013        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4014
4015        WARNING! This is too long operation if a lot of bonds requested from broker server.
4016
4017        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4018
4019        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4020                        extended information about bonds: main info, current prices, bond payment calendar,
4021                        coupon yields, current yields and some statistics etc.
4022                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4023        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4024                     for further used by data scientists or stock analytics.
4025        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4026        """
4027        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4028            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4029
4030        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4031
4032        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4033        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4034        calendar = None
4035        for bond in extBonds.iterrows():
4036            for item in bond[1]["calendar"]:
4037                cData = {
4038                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4039                    "couponDate": item["couponDate"],
4040                    "figi": bond[1]["figi"],
4041                    "ticker": bond[1]["ticker"],
4042                    "name": bond[1]["name"],
4043                    "couponNumber": item["couponNumber"],
4044                    "payOneBond": item["payOneBond"],
4045                    "payCurrency": item["payCurrency"],
4046                    "couponType": item["couponType"],
4047                    "couponPeriod": item["couponPeriod"],
4048                    "fixDate": item["fixDate"],
4049                    "couponStartDate": item["couponStartDate"],
4050                    "couponEndDate": item["couponEndDate"],
4051                }
4052
4053                if calendar is None:
4054                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4055
4056                else:
4057                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4058
4059        if calendar is not None:
4060            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4061
4062            # Saving calendar from Pandas DataFrame to XLSX sheet:
4063            if xlsx:
4064                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4065
4066                with pd.ExcelWriter(
4067                        path=xlsxCalendarFile,
4068                        date_format=TKS_DATE_FORMAT,
4069                        datetime_format=TKS_DATE_TIME_FORMAT,
4070                        mode="w",
4071                ) as writer:
4072                    humanReadable = calendar.copy(deep=True)
4073                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4074                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4075                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4076                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4077                    humanReadable.columns = colNames  # human-readable column names
4078
4079                    humanReadable.to_excel(
4080                        writer,
4081                        sheet_name="Bond payments calendar",
4082                        index=False,
4083                        encoding="UTF-8",
4084                        freeze_panes=(1, 2),
4085                    )  # saving as XLSX-file with freeze first row and column as headers
4086
4087                    del humanReadable  # release df in memory
4088
4089                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4090
4091        return calendar
4092
4093    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4094        """
4095        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4096        Also, creates Markdown file with calendar data, `calendar.md` by default.
4097
4098        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4099
4100        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4101                        extended information about bonds: main info, current prices, bond payment calendar,
4102                        coupon yields, current yields and some statistics etc.
4103                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4104        :param show: if `True` then also printing bonds payment calendar to the console,
4105                     otherwise save to file `calendarFile` only. `False` by default.
4106        :return: multilines text in Markdown format with bonds payment calendar as a table.
4107        """
4108        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4109            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4110
4111        infoText = "# Bond payments calendar\n\n"
4112
4113        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4114
4115        if not (calendar is None or calendar.empty):
4116            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4117
4118            info = [
4119                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4120                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4121            ]
4122
4123            newMonth = False
4124            notOneBond = calendar["figi"].nunique() > 1
4125            for i, bond in enumerate(calendar.iterrows()):
4126                if newMonth and notOneBond:
4127                    info.append(splitLine)
4128
4129                info.append(
4130                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4131                        "  √" if bond[1]["paid"] else "  —",
4132                        bond[1]["couponDate"].split("T")[0],
4133                        bond[1]["figi"],
4134                        bond[1]["ticker"],
4135                        bond[1]["couponNumber"],
4136                        "{} {}".format(
4137                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4138                            bond[1]["payCurrency"],
4139                        ),
4140                        bond[1]["couponType"],
4141                        bond[1]["couponPeriod"],
4142                        bond[1]["fixDate"].split("T")[0],
4143                    )
4144                )
4145
4146                if i < len(calendar.values) - 1:
4147                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4148                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4149                    newMonth = False if curDate.month == nextDate.month else True
4150
4151                else:
4152                    newMonth = False
4153
4154            infoText += "".join(info)
4155
4156            if show:
4157                uLogger.info("{}".format(infoText))
4158
4159            if self.calendarFile is not None:
4160                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4161                    fH.write(infoText)
4162
4163                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4164
4165        else:
4166            infoText += "No data\n"
4167
4168        return infoText
4169
4170    def OverviewAccounts(self, show: bool = False) -> dict:
4171        """
4172        Method for parsing and show simple table with all available user accounts.
4173
4174        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4175
4176        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4177        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4178                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4179                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4180                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4181                                                        "closed": "—", "access": "Full access" }, ...}}`
4182        """
4183        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4184
4185        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4186        accounts = {
4187            item["id"]: {
4188                "type": TKS_ACCOUNT_TYPES[item["type"]],
4189                "name": item["name"],
4190                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4191                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4192                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4193                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4194            } for item in rawAccounts["accounts"]
4195        }
4196
4197        # Raw and parsed data with some fields replaced in "stat" section:
4198        view = {
4199            "rawAccounts": rawAccounts,
4200            "stat": accounts,
4201        }
4202
4203        # --- Prepare simple text table with only accounts data in human-readable format:
4204        if show:
4205            info = [
4206                "# User accounts\n\n",
4207                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4208                "| Account ID   | Type                      | Status                    | Name                           |\n",
4209                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4210            ]
4211
4212            for account in view["stat"].keys():
4213                info.extend([
4214                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4215                        account,
4216                        view["stat"][account]["type"],
4217                        view["stat"][account]["status"],
4218                        view["stat"][account]["name"],
4219                    )
4220                ])
4221
4222            infoText = "".join(info)
4223
4224            uLogger.info(infoText)
4225
4226            if self.userAccountsFile:
4227                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4228                    fH.write(infoText)
4229
4230                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4231
4232        return view
4233
4234    def OverviewUserInfo(self, show: bool = False) -> dict:
4235        """
4236        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4237
4238        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4239
4240        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4241        :return: dict with raw parsed data from server and some calculated statistics about it.
4242        """
4243        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4244        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4245        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4246        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4247        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4248        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4249
4250        # This is dict with parsed common user data:
4251        userInfo = {
4252            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4253            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4254            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4255            "tariff": rawUserInfo["tariff"],
4256        }
4257
4258        # This is an array of dict with parsed margin statuses for every account IDs:
4259        margins = {}
4260        for accountId in accounts.keys():
4261            if rawMargins[accountId]:
4262                margins[accountId] = {
4263                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4264                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4265                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4266                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4267                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4268                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4269                }
4270
4271            else:
4272                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4273
4274        unary = {}  # unary-connection limits
4275        for item in rawTariffLimits["unaryLimits"]:
4276            if item["limitPerMinute"] in unary.keys():
4277                unary[item["limitPerMinute"]].extend(item["methods"])
4278
4279            else:
4280                unary[item["limitPerMinute"]] = item["methods"]
4281
4282        stream = {}  # stream-connection limits
4283        for item in rawTariffLimits["streamLimits"]:
4284            if item["limit"] in stream.keys():
4285                stream[item["limit"]].extend(item["streams"])
4286
4287            else:
4288                stream[item["limit"]] = item["streams"]
4289
4290        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4291        limits = {
4292            "unary": unary,
4293            "stream": stream,
4294        }
4295
4296        # Raw and parsed data as an output result:
4297        view = {
4298            "rawUserInfo": rawUserInfo,
4299            "rawAccounts": rawAccounts,
4300            "rawMargins": rawMargins,
4301            "rawTariffLimits": rawTariffLimits,
4302            "stat": {
4303                "userInfo": userInfo,
4304                "accounts": accounts,
4305                "margins": margins,
4306                "limits": limits,
4307            },
4308        }
4309
4310        # --- Prepare text table with user information in human-readable format:
4311        if show:
4312            info = [
4313                "# Full user information\n\n",
4314                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4315                "## Common information\n\n",
4316                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4317                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4318                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4319                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4320                "\n## User accounts\n\n",
4321            ]
4322
4323            for account in view["stat"]["accounts"].keys():
4324                info.extend([
4325                    "### ID: [{}]\n\n".format(account),
4326                    "| Parameters           | Values                                                       |\n",
4327                    "|----------------------|--------------------------------------------------------------|\n",
4328                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4329                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4330                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4331                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4332                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4333                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4334                ])
4335
4336                if margins[account]:
4337                    info.extend([
4338                        "| Margin status:       | Enabled                                                      |\n",
4339                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4340                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4341                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4342                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4343                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4344                    ])
4345
4346                else:
4347                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4348
4349            info.extend([
4350                "\n## Current user tariff limits\n",
4351                "\nSee also:\n",
4352                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4353                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4354                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4355                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4356                "\n### Unary limits\n",
4357            ])
4358
4359            if unary:
4360                for key, values in sorted(unary.items()):
4361                    info.append("\n* Max requests per minute: {}\n".format(key))
4362
4363                    for value in values:
4364                        info.append("  - {}\n".format(value))
4365
4366            else:
4367                info.append("\nNot available\n")
4368
4369            info.append("\n### Stream limits\n")
4370
4371            if stream:
4372                for key, values in sorted(stream.items()):
4373                    info.append("\n* Max stream connections: {}\n".format(key))
4374
4375                    for value in values:
4376                        info.append("  - {}\n".format(value))
4377
4378            else:
4379                info.append("\nNot available\n")
4380
4381            infoText = "".join(info)
4382
4383            uLogger.info(infoText)
4384
4385            if self.userInfoFile:
4386                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4387                    fH.write(infoText)
4388
4389                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4390
4391        return view
4392
4393
4394class Args:
4395    """
4396    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4397    """
4398    def __init__(self, **kwargs):
4399        self.__dict__.update(kwargs)
4400
4401    def __getattr__(self, item):
4402        return None
4403
4404
4405def ParseArgs():
4406    """This function get and parse command line keys."""
4407    parser = ArgumentParser()  # command-line string parser
4408
4409    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4410    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4411
4412    # --- options:
4413
4414    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4415    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4416    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4417
4418    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4419    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4420
4421    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4422    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4423
4424    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4425
4426    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4427    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4428    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4429
4430    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4431    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4432
4433    # --- commands:
4434
4435    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4436
4437    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4438    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4439    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4440    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4441    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4442    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4443    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4444    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4445
4446    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4447    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4448    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4449    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4450    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4451    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4452
4453    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4454    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4455    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4456    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4457
4458    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4459    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4460    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4461
4462    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4463    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4464    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4465    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4466    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4467    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4468    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4469
4470    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4471    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4472    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4473    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4474    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4475
4476    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4477    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4478    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4479
4480    cmdArgs = parser.parse_args()
4481    return cmdArgs
4482
4483
4484def Main(**kwargs):
4485    """
4486    Main function for work with TKSBrokerAPI in the console.
4487
4488    See examples:
4489    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4490    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4491    """
4492    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4493
4494    if args.debug_level:
4495        uLogger.level = 10  # always debug level by default
4496        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4497
4498    exitCode = 0
4499    start = datetime.now(tzutc())
4500    uLogger.debug("=-" * 50)
4501    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4502        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4503        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4504    ))
4505
4506    # trying to calculate full current version:
4507    buildVersion = __version__
4508    try:
4509        v = version("tksbrokerapi")
4510        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4511
4512    except Exception:
4513        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4514
4515    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4516    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4517
4518    try:
4519        if args.version:
4520            print("TKSBrokerAPI {}".format(buildVersion))
4521            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4522
4523        else:
4524            # Init class for trading with Tinkoff Broker:
4525            trader = TinkoffBrokerServer(
4526                token=args.token,
4527                accountId=args.account_id,
4528                useCache=not args.no_cache,
4529            )
4530
4531            # --- set some options:
4532
4533            if args.more:
4534                trader.moreDebug = True
4535                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4536
4537            if args.ticker:
4538                ticker = args.ticker.upper()  # Tickers may be upper case only
4539
4540                if ticker in trader.aliasesKeys:
4541                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4542
4543                else:
4544                    trader.ticker = ticker
4545
4546            if args.figi:
4547                trader.figi = args.figi.upper()  # FIGIs may be upper case only
4548
4549            if args.depth is not None:
4550                trader.depth = args.depth
4551
4552            # --- do one command:
4553
4554            if args.list:
4555                if args.output is not None:
4556                    trader.instrumentsFile = args.output
4557
4558                trader.ShowInstrumentsInfo(show=True)
4559
4560            elif args.list_xlsx:
4561                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4562
4563            elif args.bonds_xlsx is not None:
4564                if args.output is not None:
4565                    trader.bondsXLSXFile = args.output
4566
4567                if len(args.bonds_xlsx) == 0:
4568                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4569
4570                else:
4571                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4572
4573            elif args.search:
4574                if args.output is not None:
4575                    trader.searchResultsFile = args.output
4576
4577                trader.SearchInstruments(pattern=args.search[0], show=True)
4578
4579            elif args.info:
4580                if not (args.ticker or args.figi):
4581                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4582                    raise Exception("Ticker or FIGI required")
4583
4584                if args.output is not None:
4585                    trader.infoFile = args.output
4586
4587                if args.ticker:
4588                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4589
4590                else:
4591                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4592
4593            elif args.calendar is not None:
4594                if args.output is not None:
4595                    trader.calendarFile = args.output
4596
4597                if len(args.calendar) == 0:
4598                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4599
4600                else:
4601                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4602
4603                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4604
4605            elif args.price:
4606                if not (args.ticker or args.figi):
4607                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4608                    raise Exception("Ticker or FIGI required")
4609
4610                trader.GetCurrentPrices(show=True)
4611
4612            elif args.prices is not None:
4613                if args.output is not None:
4614                    trader.pricesFile = args.output
4615
4616                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4617
4618            elif args.overview:
4619                if args.output is not None:
4620                    trader.overviewFile = args.output
4621
4622                trader.Overview(show=True, details="full")
4623
4624            elif args.overview_digest:
4625                if args.output is not None:
4626                    trader.overviewDigestFile = args.output
4627
4628                trader.Overview(show=True, details="digest")
4629
4630            elif args.overview_positions:
4631                if args.output is not None:
4632                    trader.overviewPositionsFile = args.output
4633
4634                trader.Overview(show=True, details="positions")
4635
4636            elif args.overview_orders:
4637                if args.output is not None:
4638                    trader.overviewOrdersFile = args.output
4639
4640                trader.Overview(show=True, details="orders")
4641
4642            elif args.overview_analytics:
4643                if args.output is not None:
4644                    trader.overviewAnalyticsFile = args.output
4645
4646                trader.Overview(show=True, details="analytics")
4647
4648            elif args.overview_calendar:
4649                if args.output is not None:
4650                    trader.overviewAnalyticsFile = args.output
4651
4652                trader.Overview(show=True, details="calendar")
4653
4654            elif args.deals is not None:
4655                if args.output is not None:
4656                    trader.reportFile = args.output
4657
4658                if 0 <= len(args.deals) < 3:
4659                    trader.Deals(
4660                        start=args.deals[0] if len(args.deals) >= 1 else None,
4661                        end=args.deals[1] if len(args.deals) == 2 else None,
4662                        show=True,  # Always show deals report in console
4663                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4664                    )
4665
4666                else:
4667                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4668                    raise Exception("Incorrect value")
4669
4670            elif args.history is not None:
4671                if args.output is not None:
4672                    trader.historyFile = args.output
4673
4674                if 0 <= len(args.history) < 3:
4675                    dataReceived = trader.History(
4676                        start=args.history[0] if len(args.history) >= 1 else None,
4677                        end=args.history[1] if len(args.history) == 2 else None,
4678                        interval="hour" if args.interval is None or not args.interval else args.interval,
4679                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4680                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4681                        show=True,  # shows all downloaded candles in console
4682                    )
4683
4684                    if args.render_chart is not None and dataReceived is not None:
4685                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4686
4687                        trader.ShowHistoryChart(
4688                            candles=dataReceived,
4689                            interact=iChart,
4690                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4691                        )
4692
4693                else:
4694                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4695                    raise Exception("Incorrect value")
4696
4697            elif args.load_history is not None:
4698                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4699
4700                if args.render_chart is not None and histData is not None:
4701                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4702                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4703
4704                    trader.ShowHistoryChart(
4705                        candles=histData,
4706                        interact=iChart,
4707                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4708                    )
4709
4710            elif args.trade is not None:
4711                if 1 <= len(args.trade) <= 5:
4712                    trader.Trade(
4713                        operation=args.trade[0],
4714                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4715                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4716                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4717                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4718                    )
4719
4720                else:
4721                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4722
4723            elif args.buy is not None:
4724                if 0 <= len(args.buy) <= 4:
4725                    trader.Buy(
4726                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4727                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4728                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4729                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4730                    )
4731
4732                else:
4733                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4734
4735            elif args.sell is not None:
4736                if 0 <= len(args.sell) <= 4:
4737                    trader.Sell(
4738                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4739                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4740                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4741                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4742                    )
4743
4744                else:
4745                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4746
4747            elif args.order:
4748                if 4 <= len(args.order) <= 7:
4749                    trader.Order(
4750                        operation=args.order[0],
4751                        orderType=args.order[1],
4752                        lots=int(args.order[2]),
4753                        targetPrice=float(args.order[3]),
4754                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4755                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4756                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4757                    )
4758
4759                else:
4760                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4761
4762            elif args.buy_limit:
4763                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4764
4765            elif args.sell_limit:
4766                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4767
4768            elif args.buy_stop:
4769                if 2 <= len(args.buy_stop) <= 7:
4770                    trader.BuyStop(
4771                        lots=int(args.buy_stop[0]),
4772                        targetPrice=float(args.buy_stop[1]),
4773                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4774                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4775                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4776                    )
4777
4778                else:
4779                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4780
4781            elif args.sell_stop:
4782                if 2 <= len(args.sell_stop) <= 7:
4783                    trader.SellStop(
4784                        lots=int(args.sell_stop[0]),
4785                        targetPrice=float(args.sell_stop[1]),
4786                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4787                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4788                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4789                    )
4790
4791                else:
4792                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4793
4794            # elif args.buy_order_grid is not None:
4795            #     # update order grid work with api v2
4796            #     if len(args.buy_order_grid) == 2:
4797            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4798            #
4799            #         for order in orderParams:
4800            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4801            #
4802            #     else:
4803            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4804            #
4805            # elif args.sell_order_grid is not None:
4806            #     # update order grid work with api v2
4807            #     if len(args.sell_order_grid) >= 2:
4808            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4809            #
4810            #         for order in orderParams:
4811            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4812            #
4813            #     else:
4814            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4815
4816            elif args.close_order is not None:
4817                trader.CloseOrders(args.close_order)  # close only one order
4818
4819            elif args.close_orders is not None:
4820                trader.CloseOrders(args.close_orders)  # close list of orders
4821
4822            elif args.close_trade:
4823                if not (args.ticker or args.figi):
4824                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4825                    raise Exception("Ticker or FIGI required")
4826
4827                if args.ticker:
4828                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4829
4830                else:
4831                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
4832
4833            elif args.close_trades is not None:
4834                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
4835
4836            elif args.close_all is not None:
4837                trader.CloseAll(*args.close_all)
4838
4839            elif args.limits:
4840                if args.output is not None:
4841                    trader.withdrawalLimitsFile = args.output
4842
4843                trader.OverviewLimits(show=True)
4844
4845            elif args.user_info:
4846                if args.output is not None:
4847                    trader.userInfoFile = args.output
4848
4849                trader.OverviewUserInfo(show=True)
4850
4851            elif args.account:
4852                if args.output is not None:
4853                    trader.userAccountsFile = args.output
4854
4855                trader.OverviewAccounts(show=True)
4856
4857            else:
4858                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4859                raise Exception("There is no command to execute")
4860
4861    except Exception:
4862        trace = tb.format_exc()
4863        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4864            if e in trace:
4865                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4866                break
4867
4868        uLogger.debug(trace)
4869        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4870        exitCode = 255  # an error occurred, must be open a ticket for this issue
4871
4872    finally:
4873        finish = datetime.now(tzutc())
4874
4875        if exitCode == 0:
4876            if args.more:
4877                uLogger.debug("All operations were finished success (summary code is 0).")
4878
4879        else:
4880            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4881                os.path.abspath(uLog.defaultLogFile), exitCode,
4882            ))
4883
4884        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4885        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4886            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4887            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4888        ))
4889        uLogger.debug("=-" * 50)
4890
4891        if not kwargs:
4892            sys.exit(exitCode)
4893
4894        else:
4895            return exitCode
4896
4897
4898if __name__ == "__main__":
4899    Main()
def GetDatesAsString(start: str = None, end: str = None) -> tuple:
 77def GetDatesAsString(start: str = None, end: str = None) -> tuple:
 78    """
 79    Create tuple of date and time strings with timezone parsed from user-friendly date.
 80
 81    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
 82
 83    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
 84    An error exception will occur if input date has incorrect format.
 85
 86    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
 87    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
 88    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
 89    Start day may be negative integer numbers: `-1`, `-2`, `-3` — how many days ago.
 90
 91    Also, you can use keywords for start if `end=None`:
 92    `today` (from 00:00:00 to the end of current day),
 93    `yesterday` (-1 day from 00:00:00 to 23:59:59),
 94    `week` (-7 day from 00:00:00 to the end of current day),
 95    `month` (-30 day from 00:00:00 to the end of current day),
 96    `year` (-365 day from 00:00:00 to the end of current day),
 97
 98    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
 99             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
100             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
101    """
102    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
103    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
104    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
105
106    # time between start and the end of the current day:
107    if start is None or start.lower() == "today":
108        pass
109
110    # from start of the last day to the end of the last day:
111    elif start.lower() == "yesterday":
112        s -= timedelta(days=1)
113        e -= timedelta(days=1)
114
115    # week (-7 day from 00:00:00 to the end of the current day):
116    elif start.lower() == "week":
117        s -= timedelta(days=6)  # +1 current day already taken into account
118
119    # month (-30 day from 00:00:00 to the end of current day):
120    elif start.lower() == "month":
121        s -= timedelta(days=29)  # +1 current day already taken into account
122
123    # year (-365 day from 00:00:00 to the end of current day):
124    elif start.lower() == "year":
125        s -= timedelta(days=364)  # +1 current day already taken into account
126
127    # -N days ago to the end of current day:
128    elif start.startswith('-') and start[1:].isdigit():
129        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
130
131    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
132    else:
133        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
134        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
135
136    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
137    s = s.strftime(TKS_DATE_TIME_FORMAT)
138    e = e.strftime(TKS_DATE_TIME_FORMAT)
139
140    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
141
142    return s, e

Create tuple of date and time strings with timezone parsed from user-friendly date.

User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).

Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.

If start=None, end=None then return dates from yesterday to the end of the day. If start=some_date_1, end=None then return dates from some_date_1 to the end of the day. If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2. Start day may be negative integer numbers: -1, -2, -3 — how many days ago.

Also, you can use keywords for start if end=None: today (from 00:00:00 to the end of current day), yesterday (-1 day from 00:00:00 to 23:59:59), week (-7 day from 00:00:00 to the end of current day), month (-30 day from 00:00:00 to the end of current day), year (-365 day from 00:00:00 to the end of current day),

Returns

tuple with 2 strings (start, end) dates in UTC ISO time format %Y-%m-%dT%H:%M:%SZ for OpenAPI. See date and time format here: TKSEnums.TKS_DATE_TIME_FORMAT. Example: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.

class TinkoffBrokerServer:
 145class TinkoffBrokerServer:
 146    """
 147    This class implements methods to work with Tinkoff broker server.
 148
 149    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 150
 151    About `token`: https://tinkoff.github.io/investAPI/token/
 152    """
 153    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 154        """
 155        Main class init.
 156
 157        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 158        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 159                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 160        :param useCache: use default cache file with raw data to use instead of `iList`.
 161                         True by default. Cache is auto-update if new day has come.
 162                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 163        :param defaultCache: path to default cache file. `dump.json` by default.
 164        """
 165        if token is None or not token:
 166            try:
 167                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 168                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 169
 170            except KeyError:
 171                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 172                raise Exception("Token required")
 173
 174        else:
 175            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 176            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 177
 178        if accountId is None or not accountId:
 179            try:
 180                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 181                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 182
 183            except KeyError:
 184                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 185
 186        else:
 187            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 188            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 189
 190        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 191        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 192
 193        Latest version: https://pypi.org/project/tksbrokerapi/
 194        """
 195
 196        self.aliases = TKS_TICKER_ALIASES
 197        """Some aliases instead official tickers.
 198
 199        See also: `TKSEnums.TKS_TICKER_ALIASES`
 200        """
 201
 202        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 203
 204        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 205
 206        self.ticker = ""
 207        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 208
 209        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 210        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 211
 212        See also: `SearchByTicker()`, `SearchInstruments()`.
 213        """
 214
 215        self.figi = ""
 216        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 217
 218        See also: `SearchByFIGI()`, `SearchInstruments()`.
 219        """
 220
 221        self.depth = 1
 222        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 223
 224        See also: `GetCurrentPrices()`.
 225        """
 226
 227        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 228        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 229
 230        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 231        """
 232
 233        uLogger.debug("Broker API server: {}".format(self.server))
 234
 235        self.timeout = 15
 236        """Server operations timeout in seconds. Default: `15`.
 237
 238        See also: `SendAPIRequest()`.
 239        """
 240
 241        self.headers = {
 242            "Content-Type": "application/json",
 243            "accept": "application/json",
 244            "Authorization": "Bearer {}".format(self.token),
 245            "x-app-name": "Tim55667757.TKSBrokerAPI",
 246        }
 247        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 248
 249        See also: `SendAPIRequest()`.
 250        """
 251
 252        self.body = None
 253        """Request body which send to broker server. Default: `None`.
 254
 255        See also: `SendAPIRequest()`.
 256        """
 257
 258        self.moreDebug = False
 259        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 260
 261        self.historyFile = None
 262        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 263
 264        See also: `History()`.
 265        """
 266
 267        self.htmlHistoryFile = "index.html"
 268        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 269
 270        See also: `ShowHistoryChart()`.
 271        """
 272
 273        self.instrumentsFile = "instruments.md"
 274        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 275
 276        See also: `ShowInstrumentsInfo()`.
 277        """
 278
 279        self.searchResultsFile = "search-results.md"
 280        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 281
 282        See also: `SearchInstruments()`.
 283        """
 284
 285        self.pricesFile = "prices.md"
 286        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 287
 288        See also: `GetListOfPrices()`.
 289        """
 290
 291        self.infoFile = "info.md"
 292        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 293
 294        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 295        """
 296
 297        self.bondsXLSXFile = "ext-bonds.xlsx"
 298        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 299        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 300
 301        See also: `ExtendBondsData()`.
 302        """
 303
 304        self.calendarFile = "calendar.md"
 305        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 306        
 307        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 308
 309        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 310        """
 311
 312        self.overviewFile = "overview.md"
 313        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 314
 315        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 316        """
 317
 318        self.overviewDigestFile = "overview-digest.md"
 319        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 320
 321        See also: `Overview()` with parameter `details="digest"`.
 322        """
 323
 324        self.overviewPositionsFile = "overview-positions.md"
 325        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 326
 327        See also: `Overview()` with parameter `details="positions"`.
 328        """
 329
 330        self.overviewOrdersFile = "overview-orders.md"
 331        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 332
 333        See also: `Overview()` with parameter `details="orders"`.
 334        """
 335
 336        self.overviewAnalyticsFile = "overview-analytics.md"
 337        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 338
 339        See also: `Overview()` with parameter `details="analytics"`.
 340        """
 341
 342        self.overviewBondsCalendarFile = "overview-calendar.md"
 343        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 344
 345        See also: `Overview()` with parameter `details="calendar"`.
 346        """
 347
 348        self.reportFile = "deals.md"
 349        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 350
 351        See also: `Deals()`.
 352        """
 353
 354        self.withdrawalLimitsFile = "limits.md"
 355        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 356
 357        See also: `OverviewLimits()` and `RequestLimits()`.
 358        """
 359
 360        self.userInfoFile = "user-info.md"
 361        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 362
 363        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 364        """
 365
 366        self.userAccountsFile = "accounts.md"
 367        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 368
 369        See also: `OverviewAccounts()`, `RequestAccounts()`.
 370        """
 371
 372        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 373        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 374
 375        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 376
 377        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 378        """
 379
 380        self.iList = None  # init iList for raw instruments data
 381        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 382        
 383        See also: `Listing()`, `DumpInstruments()`.
 384        """
 385
 386        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 387        if useCache:
 388            if os.path.exists(self.iListDumpFile):
 389                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 390                curTime = datetime.now(tzutc())
 391
 392                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 393                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 394
 395                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 396
 397                else:
 398                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 399
 400                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 401                        os.path.abspath(self.iListDumpFile),
 402                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 403                    ))
 404
 405            else:
 406                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 407                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 408
 409        else:
 410            self.iList = self.Listing()  # request new raw instruments data from broker server
 411            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 412
 413        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 414        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 415
 416        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 417        """
 418
 419    def _ParseJSON(self, rawData="{}") -> dict:
 420        """
 421        Parse JSON from response string.
 422
 423        :param rawData: this is a string with JSON-formatted text.
 424        :return: JSON (dictionary), parsed from server response string.
 425        """
 426        responseJSON = json.loads(rawData) if rawData else {}
 427
 428        if self.moreDebug:
 429            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 430
 431        return responseJSON
 432
 433    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 434        """
 435        Send GET or POST request to broker server and receive JSON object.
 436
 437        self.header: must be defining with dictionary of headers.
 438        self.body: if define then used as request body. None by default.
 439        self.timeout: global request timeout, 15 seconds by default.
 440        :param url: url with REST request.
 441        :param reqType: send "GET" or "POST" request. "GET" by default.
 442        :param retry: how many times retry after first request if an 5xx server errors occurred.
 443        :param pause: sleep time in seconds between retries.
 444        :return: response JSON (dictionary) from broker.
 445        """
 446        if reqType not in ("GET", "POST"):
 447            uLogger.error("You can define request type: 'GET' or 'POST'!")
 448            raise Exception("Incorrect value")
 449
 450        if self.moreDebug:
 451            uLogger.debug("Request parameters:")
 452            uLogger.debug("    - REST API URL: {}".format(url))
 453            uLogger.debug("    - request type: {}".format(reqType))
 454            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 455            uLogger.debug("    - body:\n{}".format(self.body))
 456
 457        # fast hack to avoid all operations with some tickers/FIGI
 458        responseJSON = {}
 459        oK = True
 460        for item in self.exclude:
 461            if item in url:
 462                if self.moreDebug:
 463                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 464
 465                oK = False
 466                break
 467
 468        if oK:
 469            counter = 0
 470            response = None
 471            errMsg = ""
 472
 473            while not response and counter <= retry:
 474                if reqType == "GET":
 475                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 476
 477                if reqType == "POST":
 478                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 479
 480                if self.moreDebug:
 481                    uLogger.debug("Response:")
 482                    uLogger.debug("    - status code: {}".format(response.status_code))
 483                    uLogger.debug("    - reason: {}".format(response.reason))
 484                    uLogger.debug("    - body length: {}".format(len(response.text)))
 485                    uLogger.debug("    - headers:\n{}".format(response.headers))
 486
 487                # Server returns some headers:
 488                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 489                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 490                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 491                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 492                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 493                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 494                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 495                    sleep(rateLimitWait)
 496
 497                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 498                if 400 <= response.status_code < 500:
 499                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 500                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 501                    counter = retry + 1
 502
 503                if 500 <= response.status_code < 600:
 504                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 505                    uLogger.debug("    - not oK, {}".format(errMsg))
 506                    counter += 1
 507
 508                    if counter <= retry:
 509                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 510                        sleep(pause)
 511
 512            responseJSON = self._ParseJSON(rawData=response.text)
 513
 514            if errMsg:
 515                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 516                uLogger.error("    - not oK, {}".format(errMsg))
 517
 518        return responseJSON
 519
 520    def _IUpdater(self, iType: str) -> tuple:
 521        """
 522        Request instrument by type from server. See available API methods for instruments:
 523        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 524        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 525        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 526        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 527        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 528
 529        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 530        :return: tuple with iType name and list of available instruments of current type for defined user token.
 531        """
 532        result = []
 533
 534        if iType in TKS_INSTRUMENTS:
 535            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 536
 537            # all instruments have the same body in API v2 requests:
 538            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 539            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 540            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 541
 542        return iType, result
 543
 544    def _IWrapper(self, kwargs):
 545        """
 546        Wrapper runs instrument's update method `_IUpdater()`.
 547        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 548        """
 549        return self._IUpdater(**kwargs)
 550
 551    def Listing(self) -> dict:
 552        """
 553        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 554
 555        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 556        """
 557        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 558        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 559
 560        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 561        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 562        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 563
 564        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 565        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 566        poolUpdater.close()
 567
 568        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 569        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 570        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 571
 572        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 573        for iType in iList.keys():
 574            for ticker in iList[iType]:
 575                iList[iType][ticker]["type"] = iType
 576
 577                if "minPriceIncrement" in iList[iType][ticker].keys():
 578                    iList[iType][ticker]["step"] = NanoToFloat(
 579                        iList[iType][ticker]["minPriceIncrement"]["units"],
 580                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 581                    )
 582
 583                else:
 584                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 585
 586        return iList
 587
 588    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 589        """
 590        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 591
 592        See also: `DumpInstruments()`, `Listing()`.
 593
 594        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 595                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 596        """
 597        if self.iListDumpFile is None or not self.iListDumpFile:
 598            uLogger.error("Output name of dump file must be defined!")
 599            raise Exception("Filename required")
 600
 601        if not self.iList or forceUpdate:
 602            self.iList = self.Listing()
 603
 604        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 605
 606        # Save as XLSX with separated sheets for every type of instruments:
 607        with pd.ExcelWriter(
 608                path=xlsxDumpFile,
 609                date_format=TKS_DATE_FORMAT,
 610                datetime_format=TKS_DATE_TIME_FORMAT,
 611                mode="w",
 612        ) as writer:
 613            for iType in TKS_INSTRUMENTS:
 614                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 615                df = df[sorted(df)]  # sorted by column names
 616                df = df.applymap(
 617                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 618                    na_action="ignore",
 619                )  # converting numbers from nano-type to float in every cell
 620                df.to_excel(
 621                    writer,
 622                    sheet_name=iType,
 623                    encoding="UTF-8",
 624                    freeze_panes=(1, 1),
 625                )  # saving as XLSX-file with freeze first row and column as headers
 626
 627        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 628
 629    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 630        """
 631        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 632        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 633
 634        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 635
 636        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 637                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 638        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 639        """
 640        if self.iListDumpFile is None or not self.iListDumpFile:
 641            uLogger.error("Output name of dump file must be defined!")
 642            raise Exception("Filename required")
 643
 644        if not self.iList or forceUpdate:
 645            self.iList = self.Listing()
 646
 647        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 648        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 649            fH.write(jsonDump)
 650
 651        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 652
 653        return jsonDump
 654
 655    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 656        """
 657        Show information about one instrument defined by json data and prints it in Markdown format.
 658
 659        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 660
 661        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 662        :param show: if `True` then also printing information about instrument and its current price.
 663        :return: multilines text in Markdown format with information about one instrument.
 664        """
 665        splitLine = "|                                                             |                                                        |\n"
 666        infoText = ""
 667
 668        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 669            info = [
 670                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 671                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 672                "| Parameters                                                  | Values                                                 |\n",
 673                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 674                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 675                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 676            ]
 677
 678            if "sector" in iJSON.keys() and iJSON["sector"]:
 679                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 680
 681            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 682                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 683
 684            info.extend([
 685                splitLine,
 686                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 687                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 688            ])
 689
 690            if "isin" in iJSON.keys() and iJSON["isin"]:
 691                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 692
 693            if "classCode" in iJSON.keys():
 694                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 695
 696            info.extend([
 697                splitLine,
 698                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 699                splitLine,
 700                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 701                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 702                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 703            ])
 704
 705            if iJSON["figi"]:
 706                self.figi = iJSON["figi"]
 707                iJSON = iJSON | self.RequestTradingStatus()
 708
 709                info.extend([
 710                    splitLine,
 711                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 712                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 713                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 714                ])
 715
 716            info.append(splitLine)
 717
 718            if "type" in iJSON.keys() and iJSON["type"]:
 719                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 720
 721                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 722                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 723
 724            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 725                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 726
 727            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 728                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 729
 730            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 731                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 732
 733            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 734                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 735
 736            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 737                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 738
 739            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 740                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 741
 742            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 743                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 744
 745            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 746                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 747
 748            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 749                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 750
 751            if "currency" in iJSON.keys():
 752                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 753
 754            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 755                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 756
 757            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 758                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 759
 760            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 761                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 762
 763            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 764                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 765
 766            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 767                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 768
 769            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 770                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 771
 772            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 773                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 774
 775            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 776                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 777
 778            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 779                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 780
 781            iExt = None
 782            if iJSON["type"] == "Bonds":
 783                info.extend([
 784                    splitLine,
 785                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 786                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 787                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 788                        iJSON["nominal"]["currency"],
 789                    )),
 790                ])
 791
 792                if "floatingCouponFlag" in iJSON.keys():
 793                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 794
 795                if "amortizationFlag" in iJSON.keys():
 796                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 797
 798                info.append(splitLine)
 799
 800                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 801                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 802
 803                if iJSON["figi"]:
 804                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 805
 806                    info.extend([
 807                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 808                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 809                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 810                    ])
 811
 812                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 813                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 814                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 815                        iJSON["aciValue"]["currency"]
 816                    )))
 817
 818            if "currentPrice" in iJSON.keys():
 819                info.append(splitLine)
 820
 821                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 822                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 823
 824                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 825                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 826                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 827                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 828                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 829
 830                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 831                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 832
 833                info.extend([
 834                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 835                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 836                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 837                    )),
 838                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 839                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 840                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 841                    )),
 842                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 843                        "{:.2f}%{}".format(
 844                            iJSON["currentPrice"]["changes"],
 845                            " ({}{:.2f} {})".format(
 846                                "+" if bondChangesDelta > 0 else "",
 847                                bondChangesDelta,
 848                                aciCurrency
 849                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 850                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 851                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 852                                currency
 853                            ),
 854                        )
 855                    ),
 856                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 857                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 858                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 859                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 860                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 861                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 862                    )),
 863                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 864                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 865                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 866                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 867                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 868                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 869                    )),
 870                ])
 871
 872            if "lot" in iJSON.keys():
 873                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 874
 875            if "step" in iJSON.keys() and iJSON["step"] != 0:
 876                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 877
 878            # Add bond payment calendar:
 879            if iJSON["type"] == "Bonds":
 880                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 881                info.extend(["\n", strCalendar])
 882
 883            infoText += "".join(info)
 884
 885            if show:
 886                uLogger.info("{}".format(infoText))
 887
 888            else:
 889                uLogger.debug("{}".format(infoText))
 890
 891            if self.infoFile is not None:
 892                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 893                    fH.write(infoText)
 894
 895                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 896
 897        return infoText
 898
 899    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 900        """
 901        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 902
 903        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 904        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 905        :return: JSON formatted data with information about instrument.
 906        """
 907        tickerJSON = {}
 908        if self.moreDebug:
 909            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 910
 911        if not self.ticker:
 912            uLogger.warning("self.ticker variable is not be empty!")
 913
 914        else:
 915            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 916                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 917                raise Exception("Instrument not allowed")
 918
 919            if not self.iList:
 920                self.iList = self.Listing()
 921
 922            if self.ticker in self.iList["Shares"].keys():
 923                tickerJSON = self.iList["Shares"][self.ticker]
 924                if self.moreDebug:
 925                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 926
 927            elif self.ticker in self.iList["Currencies"].keys():
 928                tickerJSON = self.iList["Currencies"][self.ticker]
 929                if self.moreDebug:
 930                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 931
 932            elif self.ticker in self.iList["Bonds"].keys():
 933                tickerJSON = self.iList["Bonds"][self.ticker]
 934                if self.moreDebug:
 935                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 936
 937            elif self.ticker in self.iList["Etfs"].keys():
 938                tickerJSON = self.iList["Etfs"][self.ticker]
 939                if self.moreDebug:
 940                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 941
 942            elif self.ticker in self.iList["Futures"].keys():
 943                tickerJSON = self.iList["Futures"][self.ticker]
 944                if self.moreDebug:
 945                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 946
 947        if tickerJSON:
 948            self.figi = tickerJSON["figi"]
 949
 950            if requestPrice:
 951                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 952
 953                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 954                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 955
 956                else:
 957                    tickerJSON["currentPrice"]["changes"] = 0
 958
 959            if show:
 960                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 961
 962        else:
 963            if show:
 964                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
 965
 966        return tickerJSON
 967
 968    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 969        """
 970        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 971
 972        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 973        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 974        :return: JSON formatted data with information about instrument.
 975        """
 976        figiJSON = {}
 977        if self.moreDebug:
 978            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
 979
 980        if not self.figi:
 981            uLogger.warning("self.figi variable is not be empty!")
 982
 983        else:
 984            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 985                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
 986                raise Exception("Instrument not allowed")
 987
 988            if not self.iList:
 989                self.iList = self.Listing()
 990
 991            for item in self.iList["Shares"].keys():
 992                if self.figi == self.iList["Shares"][item]["figi"]:
 993                    figiJSON = self.iList["Shares"][item]
 994
 995                    if self.moreDebug:
 996                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
 997
 998                    break
 999
1000            if not figiJSON:
1001                for item in self.iList["Currencies"].keys():
1002                    if self.figi == self.iList["Currencies"][item]["figi"]:
1003                        figiJSON = self.iList["Currencies"][item]
1004
1005                        if self.moreDebug:
1006                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1007
1008                        break
1009
1010            if not figiJSON:
1011                for item in self.iList["Bonds"].keys():
1012                    if self.figi == self.iList["Bonds"][item]["figi"]:
1013                        figiJSON = self.iList["Bonds"][item]
1014
1015                        if self.moreDebug:
1016                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1017
1018                        break
1019
1020            if not figiJSON:
1021                for item in self.iList["Etfs"].keys():
1022                    if self.figi == self.iList["Etfs"][item]["figi"]:
1023                        figiJSON = self.iList["Etfs"][item]
1024
1025                        if self.moreDebug:
1026                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1027
1028                        break
1029
1030            if not figiJSON:
1031                for item in self.iList["Futures"].keys():
1032                    if self.figi == self.iList["Futures"][item]["figi"]:
1033                        figiJSON = self.iList["Futures"][item]
1034
1035                        if self.moreDebug:
1036                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1037
1038                        break
1039
1040        if figiJSON:
1041            self.figi = figiJSON["figi"]
1042            self.ticker = figiJSON["ticker"]
1043
1044            if requestPrice:
1045                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1046
1047                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1048                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1049
1050                else:
1051                    figiJSON["currentPrice"]["changes"] = 0
1052
1053            if show:
1054                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1055
1056        else:
1057            if show:
1058                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1059
1060        return figiJSON
1061
1062    def GetCurrentPrices(self, show: bool = True) -> dict:
1063        """
1064        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1065        `{"buy": [{"price": 1243.8, "quantity": 193},
1066                  {"price": 1244.0, "quantity": 168},
1067                  {"price": 1244.8, "quantity": 5},
1068                  {"price": 1245.0, "quantity": 61},
1069                  {"price": 1245.4, "quantity": 60}],
1070          "sell": [{"price": 1243.6, "quantity": 8},
1071                   {"price": 1242.6, "quantity": 10},
1072                   {"price": 1242.4, "quantity": 18},
1073                   {"price": 1242.2, "quantity": 50},
1074                   {"price": 1242.0, "quantity": 113}],
1075          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1076        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1077        - sell: list of dicts with Buyers prices,
1078            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1079            - quantity: volume value by current price in lots,
1080        - limitUp: current trade session limit price, maximum,
1081        - limitDown: current trade session limit price, minimum,
1082        - lastPrice: last deal price of the instrument,
1083        - closePrice: previous trade session close price of the instrument.
1084
1085        See also: `SearchByTicker()` and `SearchByFIGI()`.
1086        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1087        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1088
1089        :param show: if `True` then print DOM to log and console.
1090        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1091                 If an error occurred then returns an empty record:
1092                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1093        """
1094        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1095
1096        if self.depth < 1:
1097            uLogger.error("Depth of Market (DOM) must be >=1!")
1098            raise Exception("Incorrect value")
1099
1100        if not (self.ticker or self.figi):
1101            uLogger.error("self.ticker or self.figi variables must be defined!")
1102            raise Exception("Ticker or FIGI required")
1103
1104        if self.ticker and not self.figi:
1105            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1106            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1107
1108        if not self.ticker and self.figi:
1109            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1110            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1111
1112        if not self.figi:
1113            uLogger.error("FIGI is not defined!")
1114            raise Exception("Ticker or FIGI required")
1115
1116        else:
1117            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1118
1119            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1120            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1121            self.body = str({"figi": self.figi, "depth": self.depth})
1122            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1123
1124            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1125                # list of dicts with sellers orders:
1126                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1127
1128                # list of dicts with buyers orders:
1129                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1130
1131                # max price of instrument at this time:
1132                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1133
1134                # min price of instrument at this time:
1135                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1136
1137                # last price of deal with instrument:
1138                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1139
1140                # last close price of instrument:
1141                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1142
1143            else:
1144                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1145                uLogger.debug("Server response: {}".format(pricesResponse))
1146
1147            if show:
1148                if prices["buy"] or prices["sell"]:
1149                    info = [
1150                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1151                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1152                            self.ticker,
1153                            self.figi,
1154                            self.depth,
1155                        ),
1156                        "-" * 60, "\n",
1157                        "             Orders of Buyers | Orders of Sellers\n",
1158                        "-" * 60, "\n",
1159                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1160                        "-" * 60, "\n",
1161                    ]
1162
1163                    if not prices["buy"]:
1164                        info.append("                              | No orders!\n")
1165                        sumBuy = 0
1166
1167                    else:
1168                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1169                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1170                        for item in maxMinSorted:
1171                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1172
1173                    if not prices["sell"]:
1174                        info.append("No orders!                    |\n")
1175                        sumSell = 0
1176
1177                    else:
1178                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1179                        for item in prices["sell"]:
1180                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1181
1182                    info.extend([
1183                        "-" * 60, "\n",
1184                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1185                        "-" * 60, "\n",
1186                    ])
1187
1188                    infoText = "".join(info)
1189
1190                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1191
1192                else:
1193                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1194
1195        return prices
1196
1197    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1198        """
1199        This method get and show information about all available broker instruments for current user account.
1200        If `instrumentsFile` string is not empty then also save information to this file.
1201
1202        :param show: if `True` then print results to console, if `False` — print only to file.
1203        :return: multi-lines string with all available broker instruments
1204        """
1205        if not self.iList:
1206            self.iList = self.Listing()
1207
1208        info = [
1209            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1210            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1211        ]
1212
1213        # add instruments count by type:
1214        for iType in self.iList.keys():
1215            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1216
1217        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1218        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1219
1220        # generating info tables with all instruments by type:
1221        for iType in self.iList.keys():
1222            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1223
1224            for instrument in self.iList[iType].keys():
1225                iName = self.iList[iType][instrument]["name"]  # instrument's name
1226                if len(iName) > 57:
1227                    iName = "{}...".format(iName[:54])  # right trim for a long string
1228
1229                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1230                    self.iList[iType][instrument]["ticker"],
1231                    iName,
1232                    self.iList[iType][instrument]["figi"],
1233                    self.iList[iType][instrument]["currency"],
1234                    self.iList[iType][instrument]["lot"],
1235                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1236                ))
1237
1238        infoText = "".join(info)
1239
1240        if show:
1241            uLogger.info(infoText)
1242
1243        if self.instrumentsFile:
1244            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1245                fH.write(infoText)
1246
1247            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1248
1249        return infoText
1250
1251    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1252        """
1253        This method search and show information about instruments by part of its ticker, FIGI or name.
1254        If `searchResultsFile` string is not empty then also save information to this file.
1255
1256        :param pattern: string with part of ticker, FIGI or instrument's name.
1257        :param show: if `True` then print results to console, if `False` — return list of result only.
1258        :return: list of dictionaries with all found instruments.
1259        """
1260        if not self.iList:
1261            self.iList = self.Listing()
1262
1263        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1264        compiledPattern = re.compile(pattern, re.IGNORECASE)
1265
1266        for iType in self.iList:
1267            for instrument in self.iList[iType].values():
1268                searchResult = compiledPattern.search(" ".join(
1269                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1270                ))
1271
1272                if searchResult:
1273                    searchResults[iType][instrument["ticker"]] = instrument
1274
1275        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1276        info = [
1277            "# Search results\n\n",
1278            "* **Search pattern:** [{}]\n".format(pattern),
1279            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1280            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1281        ]
1282        infoShort = info[:]
1283
1284        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1285        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1286        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1287
1288        if resultsLen == 0:
1289            info.append("\nNo results\n")
1290            infoShort.append("\nNo results\n")
1291            uLogger.warning("No results. Try changing your search pattern.")
1292
1293        else:
1294            for iType in searchResults:
1295                iTypeValuesCount = len(searchResults[iType].values())
1296                if iTypeValuesCount > 0:
1297                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1298                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1299
1300                    for instrument in searchResults[iType].values():
1301                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1302                            instrument["type"],
1303                            instrument["ticker"],
1304                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1305                            instrument["figi"],
1306                        ))
1307
1308                    if iTypeValuesCount <= 5:
1309                        infoShort.extend(info[-iTypeValuesCount:])
1310
1311                    else:
1312                        infoShort.extend(info[-5:])
1313                        infoShort.append(skippedLine)
1314
1315        infoText = "".join(info)
1316        infoTextShort = "".join(infoShort)
1317
1318        if show:
1319            uLogger.info(infoTextShort)
1320            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1321
1322        if self.searchResultsFile:
1323            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1324                fH.write(infoText)
1325
1326            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1327
1328        return searchResults
1329
1330    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1331        """
1332        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1333
1334        :param instruments: list of strings with tickers or FIGIs.
1335        :return: list with unique instrument FIGIs only.
1336        """
1337        requestedInstruments = []
1338        for iName in instruments:
1339            if iName not in self.aliases.keys():
1340                if iName not in requestedInstruments:
1341                    requestedInstruments.append(iName)
1342
1343            else:
1344                if iName not in requestedInstruments:
1345                    if self.aliases[iName] not in requestedInstruments:
1346                        requestedInstruments.append(self.aliases[iName])
1347
1348        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1349
1350        onlyUniqueFIGIs = []
1351        for iName in requestedInstruments:
1352            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1353                continue
1354
1355            self.ticker = iName
1356            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1357
1358            if not iData:
1359                self.ticker = ""
1360                self.figi = iName
1361
1362                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1363
1364                if not iData:
1365                    self.figi = ""
1366                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1367
1368            if iData and iData["figi"] not in onlyUniqueFIGIs:
1369                onlyUniqueFIGIs.append(iData["figi"])
1370
1371        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1372
1373        return onlyUniqueFIGIs
1374
1375    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1376        """
1377        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1378
1379        See limits: https://tinkoff.github.io/investAPI/limits/
1380
1381        If `pricesFile` string is not empty then also save information to this file.
1382
1383        :param instruments: list of strings with tickers or FIGIs.
1384        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1385        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1386                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1387        """
1388        if instruments is None or not instruments:
1389            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1390            raise Exception("Ticker or FIGI required")
1391
1392        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1393
1394        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1395
1396        iList = []  # trying to get info and current prices about all unique instruments:
1397        for self.figi in onlyUniqueFIGIs:
1398            iData = self.SearchByFIGI(requestPrice=True)
1399            iList.append(iData)
1400
1401        self.ShowListOfPrices(iList, show)
1402
1403        return iList
1404
1405    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1406        """
1407        Show table contains current prices of given instruments.
1408
1409        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1410                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1411        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1412        :return: multilines text in Markdown format as a table contains current prices.
1413        """
1414        infoText = ""
1415
1416        if show or self.pricesFile:
1417            info = [
1418                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1419                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1420                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1421            ]
1422
1423            for item in iList:
1424                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1425                    item["ticker"],
1426                    item["figi"],
1427                    item["type"],
1428                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1429                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1430                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1431                    "{} / {}".format(
1432                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1433                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1434                    ),
1435                    "{} / {}".format(
1436                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1437                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1438                    ),
1439                    item["currency"],
1440                ))
1441
1442            infoText = "".join(info)
1443
1444            if show:
1445                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1446
1447            if self.pricesFile:
1448                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1449                    fH.write(infoText)
1450
1451                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1452
1453        return infoText
1454
1455    def RequestTradingStatus(self) -> dict:
1456        """
1457        Requesting trading status for the instrument defined by `figi` variable.
1458
1459        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1460
1461        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1462
1463        :return: dictionary with trading status attributes. Response example:
1464                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1465                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1466        """
1467        if self.figi is None or not self.figi:
1468            uLogger.error("Variable `figi` must be defined for using this method!")
1469            raise Exception("FIGI required")
1470
1471        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1472
1473        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1474        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1475        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1476
1477        if self.moreDebug:
1478            uLogger.debug("Records about current trading status successfully received")
1479
1480        return tradingStatus
1481
1482    def RequestPortfolio(self) -> dict:
1483        """
1484        Requesting actual user's portfolio for current `accountId`.
1485
1486        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1487
1488        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1489
1490        :return: dictionary with user's portfolio.
1491        """
1492        if self.accountId is None or not self.accountId:
1493            uLogger.error("Variable `accountId` must be defined for using this method!")
1494            raise Exception("Account ID required")
1495
1496        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1497
1498        self.body = str({"accountId": self.accountId})
1499        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1500        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1501
1502        if self.moreDebug:
1503            uLogger.debug("Records about user's portfolio successfully received")
1504
1505        return rawPortfolio
1506
1507    def RequestPositions(self) -> dict:
1508        """
1509        Requesting open positions by currencies and instruments for current `accountId`.
1510
1511        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1512
1513        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1514
1515        :return: dictionary with open positions by instruments.
1516        """
1517        if self.accountId is None or not self.accountId:
1518            uLogger.error("Variable `accountId` must be defined for using this method!")
1519            raise Exception("Account ID required")
1520
1521        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1522
1523        self.body = str({"accountId": self.accountId})
1524        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1525        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1526
1527        if self.moreDebug:
1528            uLogger.debug("Records about current open positions successfully received")
1529
1530        return rawPositions
1531
1532    def RequestPendingOrders(self) -> list:
1533        """
1534        Requesting current actual pending orders for current `accountId`.
1535
1536        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1537
1538        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1539
1540        :return: list of dictionaries with pending orders.
1541        """
1542        if self.accountId is None or not self.accountId:
1543            uLogger.error("Variable `accountId` must be defined for using this method!")
1544            raise Exception("Account ID required")
1545
1546        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1547
1548        self.body = str({"accountId": self.accountId})
1549        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1550        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1551
1552        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1553
1554        return rawOrders
1555
1556    def RequestStopOrders(self) -> list:
1557        """
1558        Requesting current actual stop orders for current `accountId`.
1559
1560        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1561
1562        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1563
1564        :return: list of dictionaries with stop orders.
1565        """
1566        if self.accountId is None or not self.accountId:
1567            uLogger.error("Variable `accountId` must be defined for using this method!")
1568            raise Exception("Account ID required")
1569
1570        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1571
1572        self.body = str({"accountId": self.accountId})
1573        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1574        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1575
1576        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1577
1578        return rawStopOrders
1579
1580    def Overview(self, show: bool = False, details: str = "full") -> dict:
1581        """
1582        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1583        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1584        and `overviewBondsCalendarFile` are defined then also save information to file.
1585
1586        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1587        many requests about the state of the portfolio, and then, based on the received data, a large number
1588        of calculation and statistics are collected.
1589
1590        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1591        :param details: how detailed should the information be?
1592        - `full` — shows full available information about portfolio status (by default),
1593        - `positions` — shows only open positions,
1594        - `orders` — shows only sections of open limits and stop orders.
1595        - `digest` — show a short digest of the portfolio status,
1596        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1597        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1598        :return: dictionary with client's raw portfolio and some statistics.
1599        """
1600        if self.accountId is None or not self.accountId:
1601            uLogger.error("Variable `accountId` must be defined for using this method!")
1602            raise Exception("Account ID required")
1603
1604        view = {
1605            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1606                "headers": {},  # list of dictionaries, response headers without "positions" section
1607                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1608                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1609                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1610                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1611                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1612                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1613                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1614                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1615                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1616            },
1617            "stat": {  # --- some statistics calculated using "raw" sections:
1618                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1619                "availableRUB": 0.,  # available rubles (without other currencies)
1620                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1621                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1622                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1623                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1624                "sharesCostRUB": 0.,  # costs of all shares in RUB
1625                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1626                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1627                "futuresCostRUB": 0.,  # costs of all futures in RUB
1628                "Currencies": [],  # list of dictionaries of all currencies statistics
1629                "Shares": [],  # list of dictionaries of all shares statistics
1630                "Bonds": [],  # list of dictionaries of all bonds statistics
1631                "Etfs": [],  # list of dictionaries of all etfs statistics
1632                "Futures": [],  # list of dictionaries of all futures statistics
1633                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1634                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1635                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1636                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1637                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1638            },
1639            "analytics": {  # --- some analytics of portfolio:
1640                "distrByAssets": {},  # portfolio distribution by assets
1641                "distrByCompanies": {},  # portfolio distribution by companies
1642                "distrBySectors": {},  # portfolio distribution by sectors
1643                "distrByCurrencies": {},  # portfolio distribution by currencies
1644                "distrByCountries": {},  # portfolio distribution by countries
1645                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1646            }
1647        }
1648
1649        details = details.lower()
1650        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1651        if details not in availableDetails:
1652            details = "full"
1653            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1654
1655        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1656
1657        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1658        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1659        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1660        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1661
1662        # save response headers without "positions" section:
1663        for key in portfolioResponse.keys():
1664            if key != "positions":
1665                view["raw"]["headers"][key] = portfolioResponse[key]
1666
1667            else:
1668                continue
1669
1670        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1671        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1672        for item in portfolioResponse["positions"]:
1673            if item["instrumentType"] == "currency":
1674                self.figi = item["figi"]
1675                curr = self.SearchByFIGI(requestPrice=False)
1676
1677                # current price of currency in RUB:
1678                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1679                    "name": curr["name"],
1680                    "currentPrice": NanoToFloat(
1681                        item["currentPrice"]["units"],
1682                        item["currentPrice"]["nano"]
1683                    ),
1684                }
1685
1686                view["raw"]["Currencies"].append(item)
1687
1688            elif item["instrumentType"] == "share":
1689                view["raw"]["Shares"].append(item)
1690
1691            elif item["instrumentType"] == "bond":
1692                view["raw"]["Bonds"].append(item)
1693
1694            elif item["instrumentType"] == "etf":
1695                view["raw"]["Etfs"].append(item)
1696
1697            elif item["instrumentType"] == "futures":
1698                view["raw"]["Futures"].append(item)
1699
1700            else:
1701                continue
1702
1703        # how many volume of currencies (by ISO currency name) are blocked:
1704        for item in view["raw"]["positions"]["blocked"]:
1705            blocked = NanoToFloat(item["units"], item["nano"])
1706            if blocked > 0:
1707                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1708
1709        # how many volume of instruments (by FIGI) are blocked:
1710        for item in view["raw"]["positions"]["securities"]:
1711            blocked = int(item["blocked"])
1712            if blocked > 0:
1713                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1714
1715        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1716
1717        if "rub" in allBlocked.keys():
1718            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1719
1720        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1721        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1722        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1723        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1724        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1725        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1726        view["stat"]["portfolioCostRUB"] = sum([
1727            view["stat"]["allCurrenciesCostRUB"],
1728            view["stat"]["sharesCostRUB"],
1729            view["stat"]["bondsCostRUB"],
1730            view["stat"]["etfsCostRUB"],
1731            view["stat"]["futuresCostRUB"],
1732        ])
1733
1734        # --- calculating some portfolio statistics:
1735        byComp = {}  # distribution by companies
1736        bySect = {}  # distribution by sectors
1737        byCurr = {}  # distribution by currencies (include RUB)
1738        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1739        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1740
1741        for item in portfolioResponse["positions"]:
1742            self.figi = item["figi"]
1743            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1744
1745            if instrument:
1746                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1747                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1748
1749                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1750                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1751
1752                else:
1753                    blocked = 0
1754
1755                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1756                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1757                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1758                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1759                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1760                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1761                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1762                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1763                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1764                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1765                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1766                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1767
1768                statData = {
1769                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1770                    "ticker": instrument["ticker"],  # ticker by FIGI
1771                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1772                    "volume": volume,  # available volume of instrument
1773                    "lots": lots,  # volume in lots of instrument
1774                    "direction": direction,  # direction of an instrument's position: short or long
1775                    "blocked": blocked,  # blocked volume of currency or instrument
1776                    "currentPrice": curPrice,  # current instrument's price in basic asset
1777                    "average": average,  # current average position price
1778                    "cost": cost,  # current cost of all volume of instrument in basic asset
1779                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1780                    "costRUB": costRUB,  # cost of instrument in ruble
1781                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1782                    "profit": profit,  # expected profit at current moment
1783                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1784                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1785                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1786                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1787                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1788                    "step": instrument["step"],  # minimum price increment
1789                }
1790
1791                # adding distribution by unique countries:
1792                if statData["country"] not in byCountry.keys():
1793                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1794
1795                else:
1796                    byCountry[statData["country"]]["cost"] += costRUB
1797                    byCountry[statData["country"]]["percent"] += percentCostRUB
1798
1799                if item["instrumentType"] != "currency":
1800                    # adding distribution by unique companies:
1801                    if statData["name"]:
1802                        if statData["name"] not in byComp.keys():
1803                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1804
1805                        else:
1806                            byComp[statData["name"]]["cost"] += costRUB
1807                            byComp[statData["name"]]["percent"] += percentCostRUB
1808
1809                    # adding distribution by unique sectors:
1810                    if statData["sector"] not in bySect.keys():
1811                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1812
1813                    else:
1814                        bySect[statData["sector"]]["cost"] += costRUB
1815                        bySect[statData["sector"]]["percent"] += percentCostRUB
1816
1817                # adding distribution by unique currencies:
1818                if currency not in byCurr.keys():
1819                    byCurr[currency] = {
1820                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1821                        "cost": costRUB,
1822                        "percent": percentCostRUB
1823                    }
1824
1825                else:
1826                    byCurr[currency]["cost"] += costRUB
1827                    byCurr[currency]["percent"] += percentCostRUB
1828
1829                # saving statistics for every instrument:
1830                if item["instrumentType"] == "currency":
1831                    view["stat"]["Currencies"].append(statData)
1832
1833                    # update dict with free funds for trading (total - blocked) by currencies
1834                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1835                    view["stat"]["funds"][currency] = {
1836                        "total": volume,
1837                        "totalCostRUB": costRUB,  # total volume cost in rubles
1838                        "free": volume - blocked,
1839                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1840                    }
1841
1842                elif item["instrumentType"] == "share":
1843                    view["stat"]["Shares"].append(statData)
1844
1845                elif item["instrumentType"] == "bond":
1846                    view["stat"]["Bonds"].append(statData)
1847
1848                elif item["instrumentType"] == "etf":
1849                    view["stat"]["Etfs"].append(statData)
1850
1851                elif item["instrumentType"] == "Futures":
1852                    view["stat"]["Futures"].append(statData)
1853
1854                else:
1855                    continue
1856
1857        # total changes in Russian Ruble:
1858        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1859        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1860        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1861        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1862        view["stat"]["funds"]["rub"] = {
1863            "total": view["stat"]["availableRUB"],
1864            "totalCostRUB": view["stat"]["availableRUB"],
1865            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1866            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1867        }
1868
1869        # --- pending orders sector data:
1870        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1871        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1872
1873        for item in view["raw"]["orders"]:
1874            self.figi = item["figi"]
1875
1876            if item["figi"] not in uniquePendingOrdersFIGIs:
1877                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1878
1879                uniquePendingOrdersFIGIs.append(item["figi"])
1880                uniquePendingOrders[item["figi"]] = instrument
1881
1882            else:
1883                instrument = uniquePendingOrders[item["figi"]]
1884
1885            if instrument:
1886                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1887                orderType = TKS_ORDER_TYPES[item["orderType"]]
1888                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1889                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1890
1891                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1892                if item["direction"] == "ORDER_DIRECTION_BUY":
1893                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1894
1895                else:
1896                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1897
1898                # requested price for order execution:
1899                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1900
1901                # necessary changes in percent to reach target from current price:
1902                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1903
1904                view["stat"]["orders"].append({
1905                    "orderID": item["orderId"],  # orderId number parameter of current order
1906                    "figi": item["figi"],  # FIGI identification
1907                    "ticker": instrument["ticker"],  # ticker name by FIGI
1908                    "lotsRequested": item["lotsRequested"],  # requested lots value
1909                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1910                    "currentPrice": lastPrice,  # current instrument's price for defined action
1911                    "targetPrice": target,  # requested price for order execution in base currency
1912                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1913                    "percentChanges": changes,  # changes in percent to target from current price
1914                    "currency": item["currency"],  # instrument's currency name
1915                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1916                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1917                    "status": orderState,  # order status from TKS_ORDER_STATES
1918                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1919                })
1920
1921        # --- stop orders sector data:
1922        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1923        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1924
1925        for item in view["raw"]["stopOrders"]:
1926            self.figi = item["figi"]
1927
1928            if item["figi"] not in uniqueStopOrdersFIGIs:
1929                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1930
1931                uniqueStopOrdersFIGIs.append(item["figi"])
1932                uniqueStopOrders[item["figi"]] = instrument
1933
1934            else:
1935                instrument = uniqueStopOrders[item["figi"]]
1936
1937            if instrument:
1938                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1939                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1940                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1941
1942                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1943                if "expirationTime" in item.keys():
1944                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1945                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1946
1947                else:
1948                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1949                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1950
1951                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1952                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1953                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1954
1955                else:
1956                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1957
1958                # requested price when stop-order executed:
1959                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1960
1961                # price for limit-order, set up when stop-order executed:
1962                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1963
1964                # necessary changes in percent to reach target from current price:
1965                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1966
1967                view["stat"]["stopOrders"].append({
1968                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1969                    "figi": item["figi"],  # FIGI identification
1970                    "ticker": instrument["ticker"],  # ticker name by FIGI
1971                    "lotsRequested": item["lotsRequested"],  # requested lots value
1972                    "currentPrice": lastPrice,  # current instrument's price for defined action
1973                    "targetPrice": target,  # requested price for stop-order execution in base currency
1974                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1975                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1976                    "percentChanges": changes,  # changes in percent to target from current price
1977                    "currency": item["currency"],  # instrument's currency name
1978                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1979                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1980                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1981                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1982                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1983                })
1984
1985        # --- calculating data for analytics section:
1986        # portfolio distribution by assets:
1987        view["analytics"]["distrByAssets"] = {
1988            "Ruble": {
1989                "uniques": 1,
1990                "cost": view["stat"]["availableRUB"],
1991                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1992            },
1993            "Currencies": {
1994                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1995                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1996                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1997            },
1998            "Shares": {
1999                "uniques": len(view["stat"]["Shares"]),
2000                "cost": view["stat"]["sharesCostRUB"],
2001                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2002            },
2003            "Bonds": {
2004                "uniques": len(view["stat"]["Bonds"]),
2005                "cost": view["stat"]["bondsCostRUB"],
2006                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2007            },
2008            "Etfs": {
2009                "uniques": len(view["stat"]["Etfs"]),
2010                "cost": view["stat"]["etfsCostRUB"],
2011                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2012            },
2013            "Futures": {
2014                "uniques": len(view["stat"]["Futures"]),
2015                "cost": view["stat"]["futuresCostRUB"],
2016                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2017            },
2018        }
2019
2020        # portfolio distribution by companies:
2021        view["analytics"]["distrByCompanies"]["All money cash"] = {
2022            "ticker": "",
2023            "cost": view["stat"]["allCurrenciesCostRUB"],
2024            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2025        }
2026        view["analytics"]["distrByCompanies"].update(byComp)
2027
2028        # portfolio distribution by sectors:
2029        view["analytics"]["distrBySectors"]["All money cash"] = {
2030            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2031            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2032        }
2033        view["analytics"]["distrBySectors"].update(bySect)
2034
2035        # portfolio distribution by currencies:
2036        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2037            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2038
2039            if self.moreDebug:
2040                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2041
2042        view["analytics"]["distrByCurrencies"].update(byCurr)
2043        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2044        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2045
2046        # portfolio distribution by countries:
2047        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2048            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2049
2050            if self.moreDebug:
2051                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2052
2053        view["analytics"]["distrByCountries"].update(byCountry)
2054        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2055        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2056
2057        # --- Prepare text statistics overview in human-readable:
2058        if show:
2059            # Whatever the value `details`, header not changes:
2060            info = [
2061                "# Client's portfolio\n\n",
2062                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2063                "* **Account ID:** [{}]\n".format(self.accountId),
2064            ]
2065
2066            if details in ["full", "positions", "digest"]:
2067                info.extend([
2068                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2069                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2070                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2071                        view["stat"]["totalChangesRUB"],
2072                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2073                        view["stat"]["totalChangesPercentRUB"],
2074                    ),
2075                ])
2076
2077            if details in ["full", "positions"]:
2078                info.extend([
2079                    "## Open positions\n\n",
2080                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2081                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2082                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2083                        "{:.2f} ({:.2f}) rub".format(
2084                            view["stat"]["availableRUB"],
2085                            view["stat"]["blockedRUB"],
2086                        )
2087                    )
2088                ])
2089
2090                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2091                    return [
2092                        "|                             |                                 |          |              |              |                     |                              |\n",
2093                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2094                            noTradeStr if noTradeStr else typeStr,
2095                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2096                        ),
2097                    ]
2098
2099                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2100                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2101                        "{} [{}]".format(data["ticker"], data["figi"]),
2102                        "{:.2f} ({:.2f}) {}".format(
2103                            data["volume"],
2104                            data["blocked"],
2105                            data["currency"],
2106                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2107                            data["volume"],
2108                            data["blocked"],
2109                        ),
2110                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2111                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2112                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2113                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2114                        "{}{:.2f} {} ({}{:.2f}%)".format(
2115                            "+" if data["profit"] > 0 else "",
2116                            data["profit"], data["baseCurrencyName"],
2117                            "+" if data["percentProfit"] > 0 else "",
2118                            data["percentProfit"],
2119                        ),
2120                    )
2121
2122                # --- Show currencies section:
2123                if view["stat"]["Currencies"]:
2124                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2125                    for item in view["stat"]["Currencies"]:
2126                        info.append(_InfoStr(item, showCurrencyName=True))
2127
2128                else:
2129                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2130
2131                # --- Show shares section:
2132                if view["stat"]["Shares"]:
2133                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2134
2135                    for item in view["stat"]["Shares"]:
2136                        info.append(_InfoStr(item))
2137
2138                else:
2139                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2140
2141                # --- Show bonds section:
2142                if view["stat"]["Bonds"]:
2143                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2144
2145                    for item in view["stat"]["Bonds"]:
2146                        info.append(_InfoStr(item))
2147
2148                else:
2149                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2150
2151                # --- Show etfs section:
2152                if view["stat"]["Etfs"]:
2153                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2154
2155                    for item in view["stat"]["Etfs"]:
2156                        info.append(_InfoStr(item))
2157
2158                else:
2159                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2160
2161                # --- Show futures section:
2162                if view["stat"]["Futures"]:
2163                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2164
2165                    for item in view["stat"]["Futures"]:
2166                        info.append(_InfoStr(item))
2167
2168                else:
2169                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2170
2171            if details in ["full", "orders"]:
2172                # --- Show pending orders section:
2173                if view["stat"]["orders"]:
2174                    info.extend([
2175                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2176                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2177                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2178                    ])
2179
2180                    for item in view["stat"]["orders"]:
2181                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2182                            "{} [{}]".format(item["ticker"], item["figi"]),
2183                            item["orderID"],
2184                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2185                            "{} {} ({}{:.2f}%)".format(
2186                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2187                                item["baseCurrencyName"],
2188                                "+" if item["percentChanges"] > 0 else "",
2189                                float(item["percentChanges"]),
2190                            ),
2191                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2192                            item["action"],
2193                            item["type"],
2194                            item["date"],
2195                        ))
2196
2197                else:
2198                    info.append("\n## Total pending limit-orders: 0\n")
2199
2200                # --- Show stop orders section:
2201                if view["stat"]["stopOrders"]:
2202                    info.extend([
2203                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2204                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2205                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2206                    ])
2207
2208                    for item in view["stat"]["stopOrders"]:
2209                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2210                            "{} [{}]".format(item["ticker"], item["figi"]),
2211                            item["orderID"],
2212                            item["lotsRequested"],
2213                            "{} {} ({}{:.2f}%)".format(
2214                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2215                                item["baseCurrencyName"],
2216                                "+" if item["percentChanges"] > 0 else "",
2217                                float(item["percentChanges"]),
2218                            ),
2219                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2220                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2221                            item["action"],
2222                            item["type"],
2223                            item["expType"],
2224                            item["createDate"],
2225                            item["expDate"],
2226                        ))
2227
2228                else:
2229                    info.append("\n## Total stop-orders: 0\n")
2230
2231            if details in ["full", "analytics"]:
2232                # -- Show analytics section:
2233                if view["stat"]["portfolioCostRUB"] > 0:
2234                    info.extend([
2235                        "\n# Analytics\n"
2236                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2237                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2238                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2239                            view["stat"]["totalChangesRUB"],
2240                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2241                            view["stat"]["totalChangesPercentRUB"],
2242                        ),
2243                        "\n## Portfolio distribution by assets\n"
2244                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2245                        "|------------------------------------|---------|---------|--------------------|\n",
2246                    ])
2247
2248                    for key in view["analytics"]["distrByAssets"].keys():
2249                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2250                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2251                                key,
2252                                view["analytics"]["distrByAssets"][key]["uniques"],
2253                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2254                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2255                            ))
2256
2257                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2258
2259                    info.extend([
2260                        "\n## Portfolio distribution by companies\n"
2261                        "\n| Company                                      | Percent | Current cost       |\n",
2262                        aSepLine,
2263                    ])
2264
2265                    for company in view["analytics"]["distrByCompanies"].keys():
2266                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2267                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2268                                "{}{}".format(
2269                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2270                                    company,
2271                                ),
2272                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2273                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2274                            ))
2275
2276                    info.extend([
2277                        "\n## Portfolio distribution by sectors\n"
2278                        "\n| Sector                                       | Percent | Current cost       |\n",
2279                        aSepLine,
2280                    ])
2281
2282                    for sector in view["analytics"]["distrBySectors"].keys():
2283                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2284                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2285                                sector,
2286                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2287                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2288                            ))
2289
2290                    info.extend([
2291                        "\n## Portfolio distribution by currencies\n"
2292                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2293                        aSepLine,
2294                    ])
2295
2296                    for curr in view["analytics"]["distrByCurrencies"].keys():
2297                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2298                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2299                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2300                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2301                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2302                            ))
2303
2304                    info.extend([
2305                        "\n## Portfolio distribution by countries\n"
2306                        "\n| Assets by country                            | Percent | Current cost       |\n",
2307                        aSepLine,
2308                    ])
2309
2310                    for country in view["analytics"]["distrByCountries"].keys():
2311                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2312                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2313                                country,
2314                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2315                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2316                            ))
2317
2318            if details in ["full", "calendar"]:
2319                # -- Show bonds payment calendar section:
2320                if view["stat"]["Bonds"]:
2321                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2322                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2323                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2324
2325                else:
2326                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2327
2328            infoText = "".join(info)
2329
2330            uLogger.info(infoText)
2331
2332            if details == "full" and self.overviewFile:
2333                filename = self.overviewFile
2334
2335            elif details == "digest" and self.overviewDigestFile:
2336                filename = self.overviewDigestFile
2337
2338            elif details == "positions" and self.overviewPositionsFile:
2339                filename = self.overviewPositionsFile
2340
2341            elif details == "orders" and self.overviewOrdersFile:
2342                filename = self.overviewOrdersFile
2343
2344            elif details == "analytics" and self.overviewAnalyticsFile:
2345                filename = self.overviewAnalyticsFile
2346
2347            elif details == "calendar" and self.overviewBondsCalendarFile:
2348                filename = self.overviewBondsCalendarFile
2349
2350            else:
2351                filename = ""
2352
2353            if filename:
2354                with open(filename, "w", encoding="UTF-8") as fH:
2355                    fH.write(infoText)
2356
2357                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2358
2359        return view
2360
2361    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2362        """
2363        Returns history operations between two given dates for current `accountId`.
2364        If `reportFile` string is not empty then also save human-readable report.
2365        Shows some statistical data of closed positions.
2366
2367        :param start: see docstring in `GetDatesAsString()` method
2368        :param end: see docstring in `GetDatesAsString()` method
2369        :param show: if `True` then also prints all records to the console.
2370        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2371        :return: original list of dictionaries with history of deals records from API ("operations" key):
2372                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2373                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2374        """
2375        if self.accountId is None or not self.accountId:
2376            uLogger.error("Variable `accountId` must be defined for using this method!")
2377            raise Exception("Account ID required")
2378
2379        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2380
2381        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2382
2383        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2384        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2385        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2386        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2387        customStat = {}  # custom statistics in additional to responseJSON
2388
2389        # --- output report in human-readable format:
2390        if show or self.reportFile:
2391            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2392            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2393            nextDay = ""
2394
2395            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2396
2397            if len(ops) > 0:
2398                customStat = {
2399                    "opsCount": 0,  # total operations count
2400                    "buyCount": 0,  # buy operations
2401                    "sellCount": 0,  # sell operations
2402                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2403                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2404                    "payIn": {"rub": 0.},  # Deposit brokerage account
2405                    "payOut": {"rub": 0.},  # Withdrawals
2406                    "divs": {"rub": 0.},  # Dividends income
2407                    "coupons": {"rub": 0.},  # Coupon's income
2408                    "brokerCom": {"rub": 0.},  # Service commissions
2409                    "serviceCom": {"rub": 0.},  # Service commissions
2410                    "marginCom": {"rub": 0.},  # Margin commissions
2411                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2412                }
2413
2414                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2415                for item in ops:
2416                    if item["state"] == "OPERATION_STATE_EXECUTED":
2417                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2418
2419                        # count buy operations:
2420                        if "_BUY" in item["operationType"]:
2421                            customStat["buyCount"] += 1
2422
2423                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2424                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2425
2426                            else:
2427                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2428
2429                        # count sell operations:
2430                        elif "_SELL" in item["operationType"]:
2431                            customStat["sellCount"] += 1
2432
2433                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2434                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2435
2436                            else:
2437                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2438
2439                        # count incoming operations:
2440                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2441                            if item["payment"]["currency"] in customStat["payIn"].keys():
2442                                customStat["payIn"][item["payment"]["currency"]] += payment
2443
2444                            else:
2445                                customStat["payIn"][item["payment"]["currency"]] = payment
2446
2447                        # count withdrawals operations:
2448                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2449                            if item["payment"]["currency"] in customStat["payOut"].keys():
2450                                customStat["payOut"][item["payment"]["currency"]] += payment
2451
2452                            else:
2453                                customStat["payOut"][item["payment"]["currency"]] = payment
2454
2455                        # count dividends income:
2456                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2457                            if item["payment"]["currency"] in customStat["divs"].keys():
2458                                customStat["divs"][item["payment"]["currency"]] += payment
2459
2460                            else:
2461                                customStat["divs"][item["payment"]["currency"]] = payment
2462
2463                        # count coupon's income:
2464                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2465                            if item["payment"]["currency"] in customStat["coupons"].keys():
2466                                customStat["coupons"][item["payment"]["currency"]] += payment
2467
2468                            else:
2469                                customStat["coupons"][item["payment"]["currency"]] = payment
2470
2471                        # count broker commissions:
2472                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2473                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2474                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2475
2476                            else:
2477                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2478
2479                        # count service commissions:
2480                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2481                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2482                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2483
2484                            else:
2485                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2486
2487                        # count margin commissions:
2488                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2489                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2490                                customStat["marginCom"][item["payment"]["currency"]] += payment
2491
2492                            else:
2493                                customStat["marginCom"][item["payment"]["currency"]] = payment
2494
2495                        # count withholding taxes:
2496                        elif "_TAX" in item["operationType"]:
2497                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2498                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2499
2500                            else:
2501                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2502
2503                        else:
2504                            continue
2505
2506                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2507
2508                # --- view "Actions" lines:
2509                info.extend([
2510                    "| Report sections            |                               |                              |                      |                        |\n",
2511                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2512                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2513                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2514                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2515                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2516                    ),
2517                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2518                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2519                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2520                    ),
2521                ])
2522
2523                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2524                for key in opsKeys:
2525                    if key == "rub":
2526                        continue
2527
2528                    info.extend([
2529                        "|                            |                               | {:<28} |                      |                        |\n".format(
2530                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2531                        ),
2532                        "|                            |                               | {:<28} |                      |                        |\n".format(
2533                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2534                        ),
2535                    ])
2536
2537                info.append(splitLine1)
2538
2539                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2540                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2541                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2542                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2543                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2544                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2545                    )
2546
2547                # --- view "Payments" lines:
2548                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2549                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2550
2551                for key in paymentsKeys:
2552                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2553
2554                info.append(splitLine1)
2555
2556                # --- view "Commissions and taxes" lines:
2557                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2558                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2559
2560                for key in comKeys:
2561                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2562
2563                info.append(splitLine1)
2564
2565                info.extend([
2566                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2567                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2568                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2569                ])
2570
2571            else:
2572                info.append("Broker returned no operations during this period\n")
2573
2574            # --- view "Operations" section:
2575            for item in ops:
2576                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2577                    continue
2578
2579                else:
2580                    self.figi = item["figi"] if item["figi"] else ""
2581                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2582                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2583
2584                    # group of deals during one day:
2585                    if nextDay and item["date"].split("T")[0] != nextDay:
2586                        info.append(splitLine2)
2587                        nextDay = ""
2588
2589                    else:
2590                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2591
2592                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2593                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2594                        self.figi if self.figi else "—",
2595                        instrument["ticker"] if instrument else "—",
2596                        instrument["type"] if instrument else "—",
2597                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2598                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2599                        TKS_OPERATION_STATES[item["state"]],
2600                        TKS_OPERATION_TYPES[item["operationType"]],
2601                    ))
2602
2603            infoText = "".join(info)
2604
2605            if show:
2606                if self.moreDebug:
2607                    uLogger.debug("Records about history of a client's operations successfully received")
2608
2609                uLogger.info(infoText)
2610
2611            if self.reportFile:
2612                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2613                    fH.write(infoText)
2614
2615                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2616
2617        return ops, customStat
2618
2619    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2620        """
2621        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2622
2623        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2624        Warning! Broker server used ISO UTC time by default.
2625
2626        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2627        Also, `historyFile` used to update history with `onlyMissing` parameter.
2628
2629        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2630
2631        :param start: see docstring in `GetDatesAsString()` method.
2632        :param end: see docstring in `GetDatesAsString()` method.
2633        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2634                         `"hour"`, `"day"`. Default: `"hour"`.
2635        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2636                            False by default. Warning! History appends only from last candle to current time
2637                            with always update last candle!
2638        :param csvSep: separator if csv-file is used, `,` by default.
2639        :param show: if `True` then also prints Pandas DataFrame to the console.
2640        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2641                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2642        """
2643        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2644        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2645        history = None  # empty pandas object for history
2646
2647        if interval not in TKS_CANDLE_INTERVALS.keys():
2648            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2649            raise Exception("Incorrect value")
2650
2651        if not (self.ticker or self.figi):
2652            uLogger.error("Ticker or FIGI must be defined!")
2653            raise Exception("Ticker or FIGI required")
2654
2655        if self.ticker and not self.figi:
2656            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2657            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2658
2659        if self.figi and not self.ticker:
2660            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2661            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2662
2663        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2664        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2665        if interval.lower() != "day":
2666            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2667
2668        delta = dtEnd - dtStart  # current UTC time minus last time in file
2669        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2670
2671        # calculate history length in candles:
2672        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2673        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2674            length += 1  # to avoid fraction time
2675
2676        # calculate data blocks count:
2677        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2678
2679        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2680        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2681        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2682        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2683        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2684
2685        tempOld = None  # pandas object for old history, if --only-missing key present
2686        lastTime = None  # datetime object of last old candle in file
2687
2688        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2689            uLogger.debug("--only-missing key present, add only last missing candles...")
2690            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2691
2692            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2693
2694            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2695            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2696            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2697            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2698
2699            # get last datetime object from last string in file or minus 1 delta if file is empty:
2700            if len(tempOld) > 0:
2701                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2702
2703            else:
2704                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2705
2706            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2707
2708        responseJSONs = []  # raw history blocks of data
2709
2710        blockEnd = dtEnd
2711        for item in range(blocks):
2712            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2713            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2714
2715            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2716                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2717            ))
2718
2719            if blockStart == blockEnd:
2720                uLogger.debug("Skipped this zero-length block...")
2721
2722            else:
2723                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2724                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2725                self.body = str({
2726                    "figi": self.figi,
2727                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2728                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2729                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2730                })
2731                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2732
2733                if "code" in responseJSON.keys():
2734                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2735
2736                else:
2737                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2738                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2739
2740                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2741
2742            blockEnd = blockStart
2743
2744        printCount = len(responseJSONs)  # candles to show in console
2745        if responseJSONs:
2746            tempHistory = pd.DataFrame(
2747                data={
2748                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2749                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2750                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2751                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2752                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2753                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2754                    "volume": [int(item["volume"]) for item in responseJSONs],
2755                },
2756                index=range(len(responseJSONs)),
2757                columns=["date", "time", "open", "high", "low", "close", "volume"],
2758            )
2759            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2760            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2761
2762            # append only newest candles to old history if --only-missing key present:
2763            if onlyMissing and tempOld is not None and lastTime is not None:
2764                index = 0  # find start index in tempHistory data:
2765
2766                for i, item in tempHistory.iterrows():
2767                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2768
2769                    if curTime == lastTime:
2770                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2771                        index = i
2772                        printCount = index + 1
2773                        break
2774
2775                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2776
2777            else:
2778                history = tempHistory  # if no `--only-missing` key then load full data from server
2779
2780            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2781
2782        if history is not None and not history.empty:
2783            if show:
2784                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2785                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2786                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2787                ))
2788
2789        else:
2790            uLogger.warning("Received an empty candles history!")
2791
2792        if self.historyFile is not None:
2793            if history is not None and not history.empty:
2794                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2795                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2796
2797            else:
2798                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2799
2800        else:
2801            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2802
2803        return history
2804
2805    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2806        """
2807        Load candles history from csv-file and return Pandas DataFrame object.
2808
2809        See also: `History()` and `ShowHistoryChart()` methods.
2810
2811        :param filePath: path to csv-file to open.
2812        """
2813        loadedHistory = None  # init candles data object
2814
2815        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2816
2817        if os.path.exists(filePath):
2818            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2819
2820            tfStr = self.priceModel.FormattedDelta(
2821                self.priceModel.timeframe,
2822                "{days} days {hours}h {minutes}m {seconds}s",
2823            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2824                self.priceModel.timeframe,
2825                "{hours}h {minutes}m {seconds}s",
2826            )
2827
2828            if loadedHistory is not None and not loadedHistory.empty:
2829                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2830                    len(loadedHistory),
2831                    tfStr,
2832                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2833                )
2834
2835            else:
2836                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2837
2838        else:
2839            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2840
2841        return loadedHistory
2842
2843    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2844        """
2845        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2846
2847        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2848        Default: `index.html` (both for interact and non-interact candlesticks chart).
2849
2850        See also: `History()` and `LoadHistory()` methods.
2851
2852        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2853        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2854                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2855                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2856                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2857        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2858                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2859        """
2860        if isinstance(candles, str):
2861            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2862            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2863
2864        elif isinstance(candles, pd.DataFrame):
2865            self.priceModel.prices = candles  # set candles chain from variable
2866            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2867
2868            if "datetime" not in candles.columns:
2869                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2870
2871        else:
2872            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2873            raise Exception("Incorrect value")
2874
2875        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2876
2877        if interact:
2878            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2879
2880            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2881
2882        else:
2883            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2884
2885            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2886
2887        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2888
2889    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2890        """
2891        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2892        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2893
2894        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2895
2896        :param operation: string "Buy" or "Sell".
2897        :param lots: volume, integer count of lots >= 1.
2898        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2899        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2900        :param expDate: string "Undefined" by default or local date in future,
2901                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2902        :return: JSON with response from broker server.
2903        """
2904        if self.accountId is None or not self.accountId:
2905            uLogger.error("Variable `accountId` must be defined for using this method!")
2906            raise Exception("Account ID required")
2907
2908        if operation is None or not operation or operation not in ("Buy", "Sell"):
2909            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2910            raise Exception("Incorrect value")
2911
2912        if lots is None or lots < 1:
2913            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2914            lots = 1
2915
2916        if tp is None or tp < 0:
2917            tp = 0
2918
2919        if sl is None or sl < 0:
2920            sl = 0
2921
2922        if expDate is None or not expDate:
2923            expDate = "Undefined"
2924
2925        if not (self.ticker or self.figi):
2926            uLogger.error("Ticker or FIGI must be defined!")
2927            raise Exception("Ticker or FIGI required")
2928
2929        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2930        self.ticker = instrument["ticker"]
2931        self.figi = instrument["figi"]
2932
2933        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2934
2935        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2936        self.body = str({
2937            "figi": self.figi,
2938            "quantity": str(lots),
2939            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2940            "accountId": str(self.accountId),
2941            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2942        })
2943        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2944
2945        if "orderId" in response.keys():
2946            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2947                operation, response["orderId"],
2948                self.ticker, self.figi, lots,
2949                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2950                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2951                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2952            ))
2953
2954            if tp > 0:
2955                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2956
2957            if sl > 0:
2958                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2959
2960        else:
2961            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2962
2963        return response
2964
2965    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2966        """
2967        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2968        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2969
2970        See also: `Order()` and `Trade()` docstrings.
2971
2972        :param lots: volume, integer count of lots >= 1.
2973        :param tp: float > 0, take profit price of stop-order.
2974        :param sl: float > 0, stop loss price of stop-order.
2975        :param expDate: it's a local date in future.
2976                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2977        :return: JSON with response from broker server.
2978        """
2979        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2980
2981    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2982        """
2983        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2984        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2985
2986        See also: `Order()` and `Trade()` docstrings.
2987
2988        :param lots: volume, integer count of lots >= 1.
2989        :param tp: float > 0, take profit price of stop-order.
2990        :param sl: float > 0, stop loss price of stop-order.
2991        :param expDate: it's a local date in the future.
2992                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2993        :return: JSON with response from broker server.
2994        """
2995        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2996
2997    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2998        """
2999        Close position of given instruments.
3000
3001        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3002        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3003                         This avoids unnecessary downloading data from the server.
3004        """
3005        if instruments is None or not instruments:
3006            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3007            raise Exception("Ticker or FIGI required")
3008
3009        if isinstance(instruments, str):
3010            instruments = [instruments]
3011
3012        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3013        if uniqueInstruments:
3014            if portfolio is None or not portfolio:
3015                portfolio = self.Overview(show=False)
3016
3017            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3018            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3019
3020            for self.figi in uniqueInstruments:
3021                if self.figi not in allOpened:
3022                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3023                    continue
3024
3025                # search open trade info about instrument by ticker:
3026                instrument = {}
3027                for iType in TKS_INSTRUMENTS:
3028                    if instrument:
3029                        break
3030
3031                    for item in portfolio["stat"][iType]:
3032                        if item["figi"] == self.figi:
3033                            instrument = item
3034                            break
3035
3036                if instrument:
3037                    self.ticker = instrument["ticker"]
3038                    self.figi = instrument["figi"]
3039
3040                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3041                        self.ticker,
3042                        self.figi,
3043                        int(instrument["volume"]),
3044                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3045                    ))
3046
3047                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3048
3049                    if tradeLots > 0:
3050                        if instrument["blocked"] > 0:
3051                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3052                                instrument["blocked"],
3053                                self.ticker,
3054                                tradeLots,
3055                            ))
3056
3057                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3058                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3059
3060                    else:
3061                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3062
3063    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3064        """
3065        Close all positions of given instruments with defined type.
3066
3067        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3068        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3069                         This avoids unnecessary downloading data from the server.
3070        """
3071        if iType not in TKS_INSTRUMENTS:
3072            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3073
3074        else:
3075            if portfolio is None or not portfolio:
3076                portfolio = self.Overview(show=False)
3077
3078            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3079            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3080
3081            if tickers and portfolio:
3082                self.CloseTrades(tickers, portfolio)
3083
3084            else:
3085                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3086
3087    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3088        """
3089        Universal method to create market or limit orders with all available parameters for current `accountId`.
3090        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3091
3092        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3093        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3094
3095        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3096        then broker immediately open market order as you can do simple --buy or --sell operations!
3097
3098        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3099        When current price will go up or down to target price value then broker opens a limit order.
3100        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3101
3102        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3103
3104        :param operation: string "Buy" or "Sell".
3105        :param orderType: string "Limit" or "Stop".
3106        :param lots: volume, integer count of lots >= 1.
3107        :param targetPrice: target price > 0. This is open trade price for limit order.
3108        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3109                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3110        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3111                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3112                         Stop loss order always executed by market price.
3113        :param expDate: string "Undefined" by default or local date in future.
3114                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3115                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3116                        A limit order has no expiration date, it lasts until the end of the trading day.
3117        :return: JSON with response from broker server.
3118        """
3119        if self.accountId is None or not self.accountId:
3120            uLogger.error("Variable `accountId` must be defined for using this method!")
3121            raise Exception("Account ID required")
3122
3123        if operation is None or not operation or operation not in ("Buy", "Sell"):
3124            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3125            raise Exception("Incorrect value")
3126
3127        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3128            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3129            raise Exception("Incorrect value")
3130
3131        if lots is None or lots < 1:
3132            uLogger.error("You must define trade volume > 0: integer count of lots!")
3133            raise Exception("Incorrect value")
3134
3135        if targetPrice is None or targetPrice <= 0:
3136            uLogger.error("Target price for limit-order must be greater than 0!")
3137            raise Exception("Incorrect value")
3138
3139        if limitPrice is None or limitPrice <= 0:
3140            limitPrice = targetPrice
3141
3142        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3143            stopType = "Limit"
3144
3145        if expDate is None or not expDate:
3146            expDate = "Undefined"
3147
3148        if not (self.ticker or self.figi):
3149            uLogger.error("Tocker or FIGI must be defined!")
3150            raise Exception("Ticker or FIGI required")
3151
3152        response = {}
3153        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3154        self.ticker = instrument["ticker"]
3155        self.figi = instrument["figi"]
3156
3157        if orderType == "Limit":
3158            uLogger.debug(
3159                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3160                    self.ticker, self.figi,
3161                    operation, lots, targetPrice, instrument["currency"],
3162                ))
3163
3164            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3165            self.body = str({
3166                "figi": self.figi,
3167                "quantity": str(lots),
3168                "price": FloatToNano(targetPrice),
3169                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3170                "accountId": str(self.accountId),
3171                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3172            })
3173            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3174
3175            if "orderId" in response.keys():
3176                uLogger.info(
3177                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3178                        response["orderId"],
3179                        self.ticker, self.figi,
3180                        operation, lots, targetPrice, instrument["currency"],
3181                    ))
3182
3183                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3184                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3185                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3186                            targetPrice, instrument["currency"],
3187                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3188                        ))
3189
3190                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3191                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3192                            targetPrice, instrument["currency"],
3193                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3194                        ))
3195
3196            else:
3197                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3198
3199        if orderType == "Stop":
3200            uLogger.debug(
3201                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3202                    self.ticker, self.figi,
3203                    operation, lots,
3204                    targetPrice, instrument["currency"],
3205                    limitPrice, instrument["currency"],
3206                    stopType, expDate,
3207                ))
3208
3209            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3210            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3211            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3212
3213            body = {
3214                "figi": self.figi,
3215                "quantity": str(lots),
3216                "price": FloatToNano(limitPrice),
3217                "stopPrice": FloatToNano(targetPrice),
3218                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3219                "accountId": str(self.accountId),
3220                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3221                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3222            }
3223
3224            if expDateUTC:
3225                body["expireDate"] = expDateUTC
3226
3227            self.body = str(body)
3228            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3229
3230            if "stopOrderId" in response.keys():
3231                uLogger.info(
3232                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3233                        response["stopOrderId"],
3234                        self.ticker, self.figi,
3235                        operation, lots,
3236                        targetPrice, instrument["currency"],
3237                        limitPrice, instrument["currency"],
3238                        TKS_STOP_ORDER_TYPES[stopOrderType],
3239                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3240                    ))
3241
3242                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3243                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3244                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3245                            targetPrice, instrument["currency"],
3246                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3247                        ))
3248
3249                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3250                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3251                            targetPrice, instrument["currency"],
3252                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3253                        ))
3254
3255            else:
3256                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3257
3258        return response
3259
3260    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3261        """
3262        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3263        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3264        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3265        See also: `Order()` docstring.
3266
3267        :param lots: volume, integer count of lots >= 1.
3268        :param targetPrice: target price > 0. This is open trade price for limit order.
3269        :return: JSON with response from broker server.
3270        """
3271        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3272
3273    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3274        """
3275        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3276        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3277        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3278        target price value then broker opens a limit order. See also: `Order()` docstring.
3279
3280        :param lots: volume, integer count of lots >= 1.
3281        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3282        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3283                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3284        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3285                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3286        :param expDate: string "Undefined" by default or local date in future.
3287                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3288                        This date is converting to UTC format for server.
3289        :return: JSON with response from broker server.
3290        """
3291        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3292
3293    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3294        """
3295        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3296        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3297        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3298        See also: `Order()` docstring.
3299
3300        :param lots: volume, integer count of lots >= 1.
3301        :param targetPrice: target price > 0. This is open trade price for limit order.
3302        :return: JSON with response from broker server.
3303        """
3304        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3305
3306    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3307        """
3308        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3309        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3310        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3311        target price value then broker opens a limit order. See also: `Order()` docstring.
3312
3313        :param lots: volume, integer count of lots >= 1.
3314        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3315        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3316                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3317        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3318                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3319        :param expDate: string "Undefined" by default or local date in future.
3320                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3321                        This date is converting to UTC format for server.
3322        :return: JSON with response from broker server.
3323        """
3324        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3325
3326    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3327        """
3328        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3329
3330        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3331        :param allOrdersIDs: pre-received lists of all active pending orders.
3332                             This avoids unnecessary downloading data from the server.
3333        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3334        """
3335        if self.accountId is None or not self.accountId:
3336            uLogger.error("Variable `accountId` must be defined for using this method!")
3337            raise Exception("Account ID required")
3338
3339        if orderIDs:
3340            if allOrdersIDs is None or not allOrdersIDs:
3341                rawOrders = self.RequestPendingOrders()
3342                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3343
3344            if allStopOrdersIDs is None or not allStopOrdersIDs:
3345                rawStopOrders = self.RequestStopOrders()
3346                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3347
3348            for orderID in orderIDs:
3349                idInPendingOrders = orderID in allOrdersIDs
3350                idInStopOrders = orderID in allStopOrdersIDs
3351
3352                if not (idInPendingOrders or idInStopOrders):
3353                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3354                    continue
3355
3356                else:
3357                    if idInPendingOrders:
3358                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3359
3360                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3361                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3362                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3363                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3364
3365                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3366                            if self.moreDebug:
3367                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3368
3369                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3370
3371                        else:
3372                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3373
3374                    elif idInStopOrders:
3375                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3376
3377                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3378                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3379                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3380                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3381
3382                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3383                            if self.moreDebug:
3384                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3385
3386                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3387
3388                        else:
3389                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3390
3391                    else:
3392                        continue
3393
3394    def CloseAllOrders(self) -> None:
3395        """
3396        Gets a list of open pending and stop orders and cancel it all.
3397        """
3398        rawOrders = self.RequestPendingOrders()
3399        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3400        lenOrders = len(allOrdersIDs)
3401
3402        rawStopOrders = self.RequestStopOrders()
3403        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3404        lenSOrders = len(allStopOrdersIDs)
3405
3406        if lenOrders > 0 or lenSOrders > 0:
3407            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3408
3409            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3410
3411        else:
3412            uLogger.info("Orders not found, nothing to cancel.")
3413
3414    def CloseAll(self, *args) -> None:
3415        """
3416        Close all available (not blocked) opened trades and orders.
3417
3418        Also, you can select one or more keywords case-insensitive:
3419        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3420
3421        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3422        """
3423        overview = self.Overview(show=False)  # get all open trades info
3424
3425        if len(args) == 0:
3426            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3427            self.CloseAllOrders()  # close all pending and stop orders
3428
3429            for iType in TKS_INSTRUMENTS:
3430                if iType != "Currencies":
3431                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3432
3433        else:
3434            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3435            lowerArgs = [x.lower() for x in args]
3436
3437            if "orders" in lowerArgs:
3438                self.CloseAllOrders()  # close all pending and stop orders
3439
3440            for iType in TKS_INSTRUMENTS:
3441                if iType.lower() in lowerArgs and iType != "Currencies":
3442                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3443
3444    @staticmethod
3445    def ParseOrderParameters(operation, **inputParameters):
3446        """
3447        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3448
3449        :param operation: string "Buy" or "Sell".
3450        :param inputParameters: this is dict of strings that looks like this
3451               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3452               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3453               "prices" key: one or more prices to open limit-orders
3454               Counts of values in lots and prices lists must be equals!
3455        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3456        """
3457        # TODO: update order grid work with api v2
3458        pass
3459        # uLogger.debug("Input parameters: {}".format(inputParameters))
3460        #
3461        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3462        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3463        #     raise Exception("Incorrect value")
3464        #
3465        # if "l" in inputParameters.keys():
3466        #     inputParameters["lots"] = inputParameters.pop("l")
3467        #
3468        # if "p" in inputParameters.keys():
3469        #     inputParameters["prices"] = inputParameters.pop("p")
3470        #
3471        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3472        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3473        #     raise Exception("Incorrect value")
3474        #
3475        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3476        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3477        #
3478        # if len(lots) != len(prices):
3479        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3480        #     raise Exception("Incorrect value")
3481        #
3482        # uLogger.debug("Extracted parameters for orders:")
3483        # uLogger.debug("lots = {}".format(lots))
3484        # uLogger.debug("prices = {}".format(prices))
3485        #
3486        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3487        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3488        # uLogger.debug("Order parameters: {}".format(result))
3489        #
3490        # return result
3491
3492    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3493        """
3494        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3495
3496        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3497        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3498        """
3499        result = False
3500        msg = "Instrument not defined!"
3501
3502        if portfolio is None or not portfolio:
3503            portfolio = self.Overview(show=False)
3504
3505        if self.ticker:
3506            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3507            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3508
3509            for iType in TKS_INSTRUMENTS:
3510                for instrument in portfolio["stat"][iType]:
3511                    if instrument["ticker"] == self.ticker:
3512                        result = True
3513                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3514                        break
3515
3516        elif self.figi:
3517            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3518            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3519
3520            for iType in TKS_INSTRUMENTS:
3521                for instrument in portfolio["stat"][iType]:
3522                    if instrument["figi"] == self.figi:
3523                        result = True
3524                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3525                        break
3526
3527        else:
3528            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3529
3530        uLogger.debug(msg)
3531
3532        return result
3533
3534    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3535        """
3536        Returns instrument from the user's portfolio if it presents there.
3537        Instrument must be defined by `ticker` (highly priority) or `figi`.
3538
3539        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3540        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3541        """
3542        result = None
3543        msg = "Instrument not defined!"
3544
3545        if portfolio is None or not portfolio:
3546            portfolio = self.Overview(show=False)
3547
3548        if self.ticker:
3549            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3550            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3551
3552            for iType in TKS_INSTRUMENTS:
3553                for instrument in portfolio["stat"][iType]:
3554                    if instrument["ticker"] == self.ticker:
3555                        result = instrument
3556                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3557                        break
3558
3559        elif self.figi:
3560            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3561            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3562
3563            for iType in TKS_INSTRUMENTS:
3564                for instrument in portfolio["stat"][iType]:
3565                    if instrument["figi"] == self.figi:
3566                        result = instrument
3567                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3568                        break
3569
3570        else:
3571            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3572
3573        uLogger.debug(msg)
3574
3575        return result
3576
3577    def RequestLimits(self) -> dict:
3578        """
3579        Method for obtaining the available funds for withdrawal for current `accountId`.
3580
3581        See also:
3582        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3583        - `OverviewLimits()` method
3584
3585        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3586                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3587                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3588                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3589        """
3590        if self.accountId is None or not self.accountId:
3591            uLogger.error("Variable `accountId` must be defined for using this method!")
3592            raise Exception("Account ID required")
3593
3594        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3595
3596        self.body = str({"accountId": self.accountId})
3597        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3598        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3599
3600        if self.moreDebug:
3601            uLogger.debug("Records about available funds for withdrawal successfully received")
3602
3603        return rawLimits
3604
3605    def OverviewLimits(self, show: bool = False) -> dict:
3606        """
3607        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3608
3609        See also: `RequestLimits()`.
3610
3611        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3612        :return: dict with raw parsed data from server and some calculated statistics about it.
3613        """
3614        if self.accountId is None or not self.accountId:
3615            uLogger.error("Variable `accountId` must be defined for using this method!")
3616            raise Exception("Account ID required")
3617
3618        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3619
3620        view = {
3621            "rawLimits": rawLimits,
3622            "limits": {  # parsed data for every currency:
3623                "money": {  # this is an array of portfolio currency positions
3624                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3625                },
3626                "blocked": {  # this is an array of blocked currency
3627                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3628                },
3629                "blockedGuarantee": {  # this is locked money under collateral for futures
3630                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3631                },
3632            },
3633        }
3634
3635        # --- Prepare text table with limits in human-readable format:
3636        if show:
3637            info = [
3638                "# Withdrawal limits\n\n",
3639                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3640                "* **Account ID:** [{}]\n".format(self.accountId),
3641            ]
3642
3643            if view["limits"]["money"]:
3644                info.extend([
3645                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3646                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3647                ])
3648
3649            else:
3650                info.append("\nNo withdrawal limits\n")
3651
3652            for curr in view["limits"]["money"].keys():
3653                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3654                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3655                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3656
3657                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3658                    "[{}]".format(curr),
3659                    "{:.2f}".format(view["limits"]["money"][curr]),
3660                    "{:.2f}".format(availableMoney),
3661                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3662                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3663                )
3664
3665                if curr == "rub":
3666                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3667
3668                else:
3669                    info.append(infoStr)
3670
3671            infoText = "".join(info)
3672
3673            uLogger.info(infoText)
3674
3675            if self.withdrawalLimitsFile:
3676                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3677                    fH.write(infoText)
3678
3679                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3680
3681        return view
3682
3683    def RequestAccounts(self) -> dict:
3684        """
3685        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3686
3687        See also:
3688        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3689        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3690        - `OverviewUserInfo()` method
3691
3692        :return: dict with raw data from server that contains accounts info. Example of dict:
3693                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3694                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3695                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3696                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3697        """
3698        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3699
3700        self.body = str({})
3701        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3702        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3703
3704        if self.moreDebug:
3705            uLogger.debug("Records about available accounts successfully received")
3706
3707        return rawAccounts
3708
3709    def RequestUserInfo(self) -> dict:
3710        """
3711        Method for requesting common user's information.
3712
3713        See also:
3714        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3715        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3716        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3717        - `OverviewUserInfo()` method
3718
3719        :return: dict with raw data from server that contains user's information. Example of dict:
3720                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3721                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3722        """
3723        uLogger.debug("Requesting common user's information. Wait, please...")
3724
3725        self.body = str({})
3726        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3727        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3728
3729        if self.moreDebug:
3730            uLogger.debug("Records about current user successfully received")
3731
3732        return rawUserInfo
3733
3734    def RequestMarginStatus(self, accountId: str = None) -> dict:
3735        """
3736        Method for requesting margin calculation for defined account ID.
3737
3738        See also:
3739        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3740        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3741        - `OverviewUserInfo()` method
3742
3743        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3744        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3745                 Example of responses:
3746                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3747                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3748                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3749                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3750                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3751                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3752        """
3753        if accountId is None or not accountId:
3754            if self.accountId is None or not self.accountId:
3755                uLogger.error("Variable `accountId` must be defined for using this method!")
3756                raise Exception("Account ID required")
3757
3758            else:
3759                accountId = self.accountId  # use `self.accountId` (main ID) by default
3760
3761        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3762
3763        self.body = str({"accountId": accountId})
3764        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3765        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3766
3767        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3768            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3769            rawMargin = {}
3770
3771        else:
3772            if self.moreDebug:
3773                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3774
3775        return rawMargin
3776
3777    def RequestTariffLimits(self) -> dict:
3778        """
3779        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3780
3781        See also:
3782        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3783        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3784        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3785        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3786        - `OverviewUserInfo()` method
3787
3788        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3789                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3790                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3791        """
3792        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3793
3794        self.body = str({})
3795        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3796        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3797
3798        if self.moreDebug:
3799            uLogger.debug("Records with limits of current tariff successfully received")
3800
3801        return rawTariffLimits
3802
3803    def RequestBondCoupons(self, iJSON: dict) -> dict:
3804        """
3805        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3806        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3807        All dates are in UTC timezone.
3808
3809        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3810        Documentation:
3811        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3812        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3813
3814        See also: `ExtendBondsData()`.
3815
3816        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3817                      If raw iJSON is not data of bond then server returns an error [400] with message:
3818                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3819        :return: dictionary with bond payment calendar. Response example
3820                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3821                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3822                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3823                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3824        """
3825        if iJSON["figi"] is None or not iJSON["figi"]:
3826            uLogger.error("FIGI must be defined for using this method!")
3827            raise Exception("FIGI required")
3828
3829        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3830        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3831
3832        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3833            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3834            self.figi,
3835            startDate,
3836            endDate,
3837        ))
3838
3839        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3840        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3841        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3842
3843        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3844            uLogger.warning("Instrument type is not bond!")
3845
3846        else:
3847            if self.moreDebug:
3848                uLogger.debug("Records about bond payment calendar successfully received")
3849
3850        return calendar
3851
3852    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3853        """
3854        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3855        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3856        coupon yields, current yields and some statistics etc.
3857
3858        WARNING! This is too long operation if a lot of bonds requested from broker server.
3859
3860        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3861
3862        :param instruments: list of strings with tickers or FIGIs.
3863        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3864                     for further used by data scientists or stock analytics.
3865        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3866                 In XLSX-file and Pandas DataFrame fields mean:
3867                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3868                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3869        """
3870        if instruments is None or not instruments:
3871            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3872            raise Exception("Ticker or FIGI required")
3873
3874        if isinstance(instruments, str):
3875            instruments = [instruments]
3876
3877        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3878
3879        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3880
3881        iCount = len(uniqueInstruments)
3882        tooLong = iCount >= 20
3883        if tooLong:
3884            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3885
3886        bonds = None
3887        for i, self.figi in enumerate(uniqueInstruments):
3888            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3889
3890            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3891                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3892                rawBond = self.SearchByFIGI(requestPrice=True)
3893
3894                # Widen raw data with UTC current time (iData["actualDateTime"]):
3895                actualDate = datetime.now(tzutc())
3896                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3897
3898                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3899                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3900
3901                # Replace some values with human-readable:
3902                iData["nominalCurrency"] = iData["nominal"]["currency"]
3903                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3904                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3905                iData["aciCurrency"] = iData["aciValue"]["currency"]
3906                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3907                iData["issueSize"] = int(iData["issueSize"])
3908                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3909                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3910                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3911                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3912                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3913                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3914                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3915                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3916                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3917                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3918
3919                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3920                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3921                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3922                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3923                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3924                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3925                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3926                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3927                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3928                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3929                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3930
3931                # Widen raw data with calendar data from `rawCalendar` values:
3932                calendarData = []
3933                if "events" in iData["rawCalendar"].keys():
3934                    for item in iData["rawCalendar"]["events"]:
3935                        calendarData.append({
3936                            "couponDate": item["couponDate"],
3937                            "couponNumber": int(item["couponNumber"]),
3938                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3939                            "payCurrency": item["payOneBond"]["currency"],
3940                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3941                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3942                            "couponStartDate": item["couponStartDate"],
3943                            "couponEndDate": item["couponEndDate"],
3944                            "couponPeriod": item["couponPeriod"],
3945                        })
3946
3947                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3948                    if "maturityDate" not in iData.keys():
3949                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3950
3951                # Widen raw data with Coupon Rate.
3952                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3953                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3954                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3955                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3956
3957                # Widen raw data with Yield to Maturity (YTM) on current date.
3958                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3959                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3960                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3961                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3962                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3963                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3964
3965                iData["calendar"] = calendarData  # adds calendar at the end
3966
3967                # Remove not used data:
3968                iData.pop("uid")
3969                iData.pop("positionUid")
3970                iData.pop("currentPrice")
3971                iData.pop("rawCalendar")
3972
3973                colNames = list(iData.keys())
3974                if bonds is None:
3975                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3976
3977                else:
3978                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3979
3980            else:
3981                uLogger.warning("Instrument is not a bond!")
3982
3983            processed = round(100 * (i + 1) / iCount, 1)
3984            if tooLong and processed % 5 == 0:
3985                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3986
3987            else:
3988                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3989
3990        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3991
3992        # Saving bonds from Pandas DataFrame to XLSX sheet:
3993        if xlsx and self.bondsXLSXFile:
3994            with pd.ExcelWriter(
3995                    path=self.bondsXLSXFile,
3996                    date_format=TKS_DATE_FORMAT,
3997                    datetime_format=TKS_DATE_TIME_FORMAT,
3998                    mode="w",
3999            ) as writer:
4000                bonds.to_excel(
4001                    writer,
4002                    sheet_name="Extended bonds data",
4003                    index=True,
4004                    encoding="UTF-8",
4005                    freeze_panes=(1, 1),
4006                )  # saving as XLSX-file with freeze first row and column as headers
4007
4008            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4009
4010        return bonds
4011
4012    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4013        """
4014        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4015
4016        WARNING! This is too long operation if a lot of bonds requested from broker server.
4017
4018        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4019
4020        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4021                        extended information about bonds: main info, current prices, bond payment calendar,
4022                        coupon yields, current yields and some statistics etc.
4023                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4024        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4025                     for further used by data scientists or stock analytics.
4026        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4027        """
4028        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4029            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4030
4031        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4032
4033        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4034        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4035        calendar = None
4036        for bond in extBonds.iterrows():
4037            for item in bond[1]["calendar"]:
4038                cData = {
4039                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4040                    "couponDate": item["couponDate"],
4041                    "figi": bond[1]["figi"],
4042                    "ticker": bond[1]["ticker"],
4043                    "name": bond[1]["name"],
4044                    "couponNumber": item["couponNumber"],
4045                    "payOneBond": item["payOneBond"],
4046                    "payCurrency": item["payCurrency"],
4047                    "couponType": item["couponType"],
4048                    "couponPeriod": item["couponPeriod"],
4049                    "fixDate": item["fixDate"],
4050                    "couponStartDate": item["couponStartDate"],
4051                    "couponEndDate": item["couponEndDate"],
4052                }
4053
4054                if calendar is None:
4055                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4056
4057                else:
4058                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4059
4060        if calendar is not None:
4061            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4062
4063            # Saving calendar from Pandas DataFrame to XLSX sheet:
4064            if xlsx:
4065                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4066
4067                with pd.ExcelWriter(
4068                        path=xlsxCalendarFile,
4069                        date_format=TKS_DATE_FORMAT,
4070                        datetime_format=TKS_DATE_TIME_FORMAT,
4071                        mode="w",
4072                ) as writer:
4073                    humanReadable = calendar.copy(deep=True)
4074                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4075                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4076                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4077                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4078                    humanReadable.columns = colNames  # human-readable column names
4079
4080                    humanReadable.to_excel(
4081                        writer,
4082                        sheet_name="Bond payments calendar",
4083                        index=False,
4084                        encoding="UTF-8",
4085                        freeze_panes=(1, 2),
4086                    )  # saving as XLSX-file with freeze first row and column as headers
4087
4088                    del humanReadable  # release df in memory
4089
4090                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4091
4092        return calendar
4093
4094    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4095        """
4096        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4097        Also, creates Markdown file with calendar data, `calendar.md` by default.
4098
4099        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4100
4101        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4102                        extended information about bonds: main info, current prices, bond payment calendar,
4103                        coupon yields, current yields and some statistics etc.
4104                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4105        :param show: if `True` then also printing bonds payment calendar to the console,
4106                     otherwise save to file `calendarFile` only. `False` by default.
4107        :return: multilines text in Markdown format with bonds payment calendar as a table.
4108        """
4109        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4110            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4111
4112        infoText = "# Bond payments calendar\n\n"
4113
4114        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4115
4116        if not (calendar is None or calendar.empty):
4117            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4118
4119            info = [
4120                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4121                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4122            ]
4123
4124            newMonth = False
4125            notOneBond = calendar["figi"].nunique() > 1
4126            for i, bond in enumerate(calendar.iterrows()):
4127                if newMonth and notOneBond:
4128                    info.append(splitLine)
4129
4130                info.append(
4131                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4132                        "  √" if bond[1]["paid"] else "  —",
4133                        bond[1]["couponDate"].split("T")[0],
4134                        bond[1]["figi"],
4135                        bond[1]["ticker"],
4136                        bond[1]["couponNumber"],
4137                        "{} {}".format(
4138                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4139                            bond[1]["payCurrency"],
4140                        ),
4141                        bond[1]["couponType"],
4142                        bond[1]["couponPeriod"],
4143                        bond[1]["fixDate"].split("T")[0],
4144                    )
4145                )
4146
4147                if i < len(calendar.values) - 1:
4148                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4149                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4150                    newMonth = False if curDate.month == nextDate.month else True
4151
4152                else:
4153                    newMonth = False
4154
4155            infoText += "".join(info)
4156
4157            if show:
4158                uLogger.info("{}".format(infoText))
4159
4160            if self.calendarFile is not None:
4161                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4162                    fH.write(infoText)
4163
4164                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4165
4166        else:
4167            infoText += "No data\n"
4168
4169        return infoText
4170
4171    def OverviewAccounts(self, show: bool = False) -> dict:
4172        """
4173        Method for parsing and show simple table with all available user accounts.
4174
4175        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4176
4177        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4178        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4179                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4180                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4181                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4182                                                        "closed": "—", "access": "Full access" }, ...}}`
4183        """
4184        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4185
4186        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4187        accounts = {
4188            item["id"]: {
4189                "type": TKS_ACCOUNT_TYPES[item["type"]],
4190                "name": item["name"],
4191                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4192                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4193                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4194                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4195            } for item in rawAccounts["accounts"]
4196        }
4197
4198        # Raw and parsed data with some fields replaced in "stat" section:
4199        view = {
4200            "rawAccounts": rawAccounts,
4201            "stat": accounts,
4202        }
4203
4204        # --- Prepare simple text table with only accounts data in human-readable format:
4205        if show:
4206            info = [
4207                "# User accounts\n\n",
4208                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4209                "| Account ID   | Type                      | Status                    | Name                           |\n",
4210                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4211            ]
4212
4213            for account in view["stat"].keys():
4214                info.extend([
4215                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4216                        account,
4217                        view["stat"][account]["type"],
4218                        view["stat"][account]["status"],
4219                        view["stat"][account]["name"],
4220                    )
4221                ])
4222
4223            infoText = "".join(info)
4224
4225            uLogger.info(infoText)
4226
4227            if self.userAccountsFile:
4228                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4229                    fH.write(infoText)
4230
4231                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4232
4233        return view
4234
4235    def OverviewUserInfo(self, show: bool = False) -> dict:
4236        """
4237        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4238
4239        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4240
4241        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4242        :return: dict with raw parsed data from server and some calculated statistics about it.
4243        """
4244        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4245        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4246        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4247        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4248        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4249        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4250
4251        # This is dict with parsed common user data:
4252        userInfo = {
4253            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4254            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4255            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4256            "tariff": rawUserInfo["tariff"],
4257        }
4258
4259        # This is an array of dict with parsed margin statuses for every account IDs:
4260        margins = {}
4261        for accountId in accounts.keys():
4262            if rawMargins[accountId]:
4263                margins[accountId] = {
4264                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4265                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4266                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4267                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4268                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4269                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4270                }
4271
4272            else:
4273                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4274
4275        unary = {}  # unary-connection limits
4276        for item in rawTariffLimits["unaryLimits"]:
4277            if item["limitPerMinute"] in unary.keys():
4278                unary[item["limitPerMinute"]].extend(item["methods"])
4279
4280            else:
4281                unary[item["limitPerMinute"]] = item["methods"]
4282
4283        stream = {}  # stream-connection limits
4284        for item in rawTariffLimits["streamLimits"]:
4285            if item["limit"] in stream.keys():
4286                stream[item["limit"]].extend(item["streams"])
4287
4288            else:
4289                stream[item["limit"]] = item["streams"]
4290
4291        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4292        limits = {
4293            "unary": unary,
4294            "stream": stream,
4295        }
4296
4297        # Raw and parsed data as an output result:
4298        view = {
4299            "rawUserInfo": rawUserInfo,
4300            "rawAccounts": rawAccounts,
4301            "rawMargins": rawMargins,
4302            "rawTariffLimits": rawTariffLimits,
4303            "stat": {
4304                "userInfo": userInfo,
4305                "accounts": accounts,
4306                "margins": margins,
4307                "limits": limits,
4308            },
4309        }
4310
4311        # --- Prepare text table with user information in human-readable format:
4312        if show:
4313            info = [
4314                "# Full user information\n\n",
4315                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4316                "## Common information\n\n",
4317                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4318                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4319                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4320                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4321                "\n## User accounts\n\n",
4322            ]
4323
4324            for account in view["stat"]["accounts"].keys():
4325                info.extend([
4326                    "### ID: [{}]\n\n".format(account),
4327                    "| Parameters           | Values                                                       |\n",
4328                    "|----------------------|--------------------------------------------------------------|\n",
4329                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4330                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4331                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4332                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4333                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4334                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4335                ])
4336
4337                if margins[account]:
4338                    info.extend([
4339                        "| Margin status:       | Enabled                                                      |\n",
4340                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4341                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4342                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4343                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4344                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4345                    ])
4346
4347                else:
4348                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4349
4350            info.extend([
4351                "\n## Current user tariff limits\n",
4352                "\nSee also:\n",
4353                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4354                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4355                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4356                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4357                "\n### Unary limits\n",
4358            ])
4359
4360            if unary:
4361                for key, values in sorted(unary.items()):
4362                    info.append("\n* Max requests per minute: {}\n".format(key))
4363
4364                    for value in values:
4365                        info.append("  - {}\n".format(value))
4366
4367            else:
4368                info.append("\nNot available\n")
4369
4370            info.append("\n### Stream limits\n")
4371
4372            if stream:
4373                for key, values in sorted(stream.items()):
4374                    info.append("\n* Max stream connections: {}\n".format(key))
4375
4376                    for value in values:
4377                        info.append("  - {}\n".format(value))
4378
4379            else:
4380                info.append("\nNot available\n")
4381
4382            infoText = "".join(info)
4383
4384            uLogger.info(infoText)
4385
4386            if self.userInfoFile:
4387                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4388                    fH.write(infoText)
4389
4390                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4391
4392        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
153    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
154        """
155        Main class init.
156
157        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
158        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
159                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
160        :param useCache: use default cache file with raw data to use instead of `iList`.
161                         True by default. Cache is auto-update if new day has come.
162                         If you don't want to use cache and always updates raw data then set `useCache=False`.
163        :param defaultCache: path to default cache file. `dump.json` by default.
164        """
165        if token is None or not token:
166            try:
167                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
168                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
169
170            except KeyError:
171                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
172                raise Exception("Token required")
173
174        else:
175            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
176            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
177
178        if accountId is None or not accountId:
179            try:
180                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
181                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
182
183            except KeyError:
184                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
185
186        else:
187            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
188            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
189
190        self.version = __version__  # duplicate here used TKSBrokerAPI main version
191        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
192
193        Latest version: https://pypi.org/project/tksbrokerapi/
194        """
195
196        self.aliases = TKS_TICKER_ALIASES
197        """Some aliases instead official tickers.
198
199        See also: `TKSEnums.TKS_TICKER_ALIASES`
200        """
201
202        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
203
204        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
205
206        self.ticker = ""
207        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
208
209        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
210        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
211
212        See also: `SearchByTicker()`, `SearchInstruments()`.
213        """
214
215        self.figi = ""
216        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
217
218        See also: `SearchByFIGI()`, `SearchInstruments()`.
219        """
220
221        self.depth = 1
222        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
223
224        See also: `GetCurrentPrices()`.
225        """
226
227        self.server = r"https://invest-public-api.tinkoff.ru/rest"
228        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
229
230        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
231        """
232
233        uLogger.debug("Broker API server: {}".format(self.server))
234
235        self.timeout = 15
236        """Server operations timeout in seconds. Default: `15`.
237
238        See also: `SendAPIRequest()`.
239        """
240
241        self.headers = {
242            "Content-Type": "application/json",
243            "accept": "application/json",
244            "Authorization": "Bearer {}".format(self.token),
245            "x-app-name": "Tim55667757.TKSBrokerAPI",
246        }
247        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
248
249        See also: `SendAPIRequest()`.
250        """
251
252        self.body = None
253        """Request body which send to broker server. Default: `None`.
254
255        See also: `SendAPIRequest()`.
256        """
257
258        self.moreDebug = False
259        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
260
261        self.historyFile = None
262        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
263
264        See also: `History()`.
265        """
266
267        self.htmlHistoryFile = "index.html"
268        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
269
270        See also: `ShowHistoryChart()`.
271        """
272
273        self.instrumentsFile = "instruments.md"
274        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
275
276        See also: `ShowInstrumentsInfo()`.
277        """
278
279        self.searchResultsFile = "search-results.md"
280        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
281
282        See also: `SearchInstruments()`.
283        """
284
285        self.pricesFile = "prices.md"
286        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
287
288        See also: `GetListOfPrices()`.
289        """
290
291        self.infoFile = "info.md"
292        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
293
294        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
295        """
296
297        self.bondsXLSXFile = "ext-bonds.xlsx"
298        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
299        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
300
301        See also: `ExtendBondsData()`.
302        """
303
304        self.calendarFile = "calendar.md"
305        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
306        
307        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
308
309        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
310        """
311
312        self.overviewFile = "overview.md"
313        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
314
315        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
316        """
317
318        self.overviewDigestFile = "overview-digest.md"
319        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
320
321        See also: `Overview()` with parameter `details="digest"`.
322        """
323
324        self.overviewPositionsFile = "overview-positions.md"
325        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
326
327        See also: `Overview()` with parameter `details="positions"`.
328        """
329
330        self.overviewOrdersFile = "overview-orders.md"
331        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
332
333        See also: `Overview()` with parameter `details="orders"`.
334        """
335
336        self.overviewAnalyticsFile = "overview-analytics.md"
337        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
338
339        See also: `Overview()` with parameter `details="analytics"`.
340        """
341
342        self.overviewBondsCalendarFile = "overview-calendar.md"
343        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
344
345        See also: `Overview()` with parameter `details="calendar"`.
346        """
347
348        self.reportFile = "deals.md"
349        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
350
351        See also: `Deals()`.
352        """
353
354        self.withdrawalLimitsFile = "limits.md"
355        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
356
357        See also: `OverviewLimits()` and `RequestLimits()`.
358        """
359
360        self.userInfoFile = "user-info.md"
361        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
362
363        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
364        """
365
366        self.userAccountsFile = "accounts.md"
367        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
368
369        See also: `OverviewAccounts()`, `RequestAccounts()`.
370        """
371
372        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
373        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
374
375        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
376
377        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
378        """
379
380        self.iList = None  # init iList for raw instruments data
381        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
382        
383        See also: `Listing()`, `DumpInstruments()`.
384        """
385
386        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
387        if useCache:
388            if os.path.exists(self.iListDumpFile):
389                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
390                curTime = datetime.now(tzutc())
391
392                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
393                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
394
395                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
396
397                else:
398                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
399
400                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
401                        os.path.abspath(self.iListDumpFile),
402                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
403                    ))
404
405            else:
406                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
407                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
408
409        else:
410            self.iList = self.Listing()  # request new raw instruments data from broker server
411            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
412
413        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
414        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
415
416        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
417        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

ticker

String with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
433    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
434        """
435        Send GET or POST request to broker server and receive JSON object.
436
437        self.header: must be defining with dictionary of headers.
438        self.body: if define then used as request body. None by default.
439        self.timeout: global request timeout, 15 seconds by default.
440        :param url: url with REST request.
441        :param reqType: send "GET" or "POST" request. "GET" by default.
442        :param retry: how many times retry after first request if an 5xx server errors occurred.
443        :param pause: sleep time in seconds between retries.
444        :return: response JSON (dictionary) from broker.
445        """
446        if reqType not in ("GET", "POST"):
447            uLogger.error("You can define request type: 'GET' or 'POST'!")
448            raise Exception("Incorrect value")
449
450        if self.moreDebug:
451            uLogger.debug("Request parameters:")
452            uLogger.debug("    - REST API URL: {}".format(url))
453            uLogger.debug("    - request type: {}".format(reqType))
454            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
455            uLogger.debug("    - body:\n{}".format(self.body))
456
457        # fast hack to avoid all operations with some tickers/FIGI
458        responseJSON = {}
459        oK = True
460        for item in self.exclude:
461            if item in url:
462                if self.moreDebug:
463                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
464
465                oK = False
466                break
467
468        if oK:
469            counter = 0
470            response = None
471            errMsg = ""
472
473            while not response and counter <= retry:
474                if reqType == "GET":
475                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
476
477                if reqType == "POST":
478                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
479
480                if self.moreDebug:
481                    uLogger.debug("Response:")
482                    uLogger.debug("    - status code: {}".format(response.status_code))
483                    uLogger.debug("    - reason: {}".format(response.reason))
484                    uLogger.debug("    - body length: {}".format(len(response.text)))
485                    uLogger.debug("    - headers:\n{}".format(response.headers))
486
487                # Server returns some headers:
488                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
489                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
490                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
491                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
492                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
493                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
494                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
495                    sleep(rateLimitWait)
496
497                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
498                if 400 <= response.status_code < 500:
499                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
500                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
501                    counter = retry + 1
502
503                if 500 <= response.status_code < 600:
504                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
505                    uLogger.debug("    - not oK, {}".format(errMsg))
506                    counter += 1
507
508                    if counter <= retry:
509                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
510                        sleep(pause)
511
512            responseJSON = self._ParseJSON(rawData=response.text)
513
514            if errMsg:
515                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
516                uLogger.error("    - not oK, {}".format(errMsg))
517
518        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
551    def Listing(self) -> dict:
552        """
553        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
554
555        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
556        """
557        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
558        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
559
560        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
561        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
562        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
563
564        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
565        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
566        poolUpdater.close()
567
568        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
569        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
570        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
571
572        # calculate minimum price increment (step) for all instruments and set up instrument's type:
573        for iType in iList.keys():
574            for ticker in iList[iType]:
575                iList[iType][ticker]["type"] = iType
576
577                if "minPriceIncrement" in iList[iType][ticker].keys():
578                    iList[iType][ticker]["step"] = NanoToFloat(
579                        iList[iType][ticker]["minPriceIncrement"]["units"],
580                        iList[iType][ticker]["minPriceIncrement"]["nano"],
581                    )
582
583                else:
584                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
585
586        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
588    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
589        """
590        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
591
592        See also: `DumpInstruments()`, `Listing()`.
593
594        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
595                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
596        """
597        if self.iListDumpFile is None or not self.iListDumpFile:
598            uLogger.error("Output name of dump file must be defined!")
599            raise Exception("Filename required")
600
601        if not self.iList or forceUpdate:
602            self.iList = self.Listing()
603
604        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
605
606        # Save as XLSX with separated sheets for every type of instruments:
607        with pd.ExcelWriter(
608                path=xlsxDumpFile,
609                date_format=TKS_DATE_FORMAT,
610                datetime_format=TKS_DATE_TIME_FORMAT,
611                mode="w",
612        ) as writer:
613            for iType in TKS_INSTRUMENTS:
614                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
615                df = df[sorted(df)]  # sorted by column names
616                df = df.applymap(
617                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
618                    na_action="ignore",
619                )  # converting numbers from nano-type to float in every cell
620                df.to_excel(
621                    writer,
622                    sheet_name=iType,
623                    encoding="UTF-8",
624                    freeze_panes=(1, 1),
625                )  # saving as XLSX-file with freeze first row and column as headers
626
627        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
629    def DumpInstruments(self, forceUpdate: bool = True) -> str:
630        """
631        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
632        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
633
634        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
635
636        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
637                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
638        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
639        """
640        if self.iListDumpFile is None or not self.iListDumpFile:
641            uLogger.error("Output name of dump file must be defined!")
642            raise Exception("Filename required")
643
644        if not self.iList or forceUpdate:
645            self.iList = self.Listing()
646
647        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
648        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
649            fH.write(jsonDump)
650
651        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
652
653        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
655    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
656        """
657        Show information about one instrument defined by json data and prints it in Markdown format.
658
659        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
660
661        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
662        :param show: if `True` then also printing information about instrument and its current price.
663        :return: multilines text in Markdown format with information about one instrument.
664        """
665        splitLine = "|                                                             |                                                        |\n"
666        infoText = ""
667
668        if iJSON is not None and iJSON and isinstance(iJSON, dict):
669            info = [
670                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
671                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
672                "| Parameters                                                  | Values                                                 |\n",
673                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
674                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
675                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
676            ]
677
678            if "sector" in iJSON.keys() and iJSON["sector"]:
679                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
680
681            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
682                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
683
684            info.extend([
685                splitLine,
686                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
687                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
688            ])
689
690            if "isin" in iJSON.keys() and iJSON["isin"]:
691                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
692
693            if "classCode" in iJSON.keys():
694                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
695
696            info.extend([
697                splitLine,
698                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
699                splitLine,
700                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
701                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
702                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
703            ])
704
705            if iJSON["figi"]:
706                self.figi = iJSON["figi"]
707                iJSON = iJSON | self.RequestTradingStatus()
708
709                info.extend([
710                    splitLine,
711                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
712                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
713                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
714                ])
715
716            info.append(splitLine)
717
718            if "type" in iJSON.keys() and iJSON["type"]:
719                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
720
721                if "shareType" in iJSON.keys() and iJSON["shareType"]:
722                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
723
724            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
725                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
726
727            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
728                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
729
730            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
731                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
732
733            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
734                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
735
736            if "focusType" in iJSON.keys() and iJSON["focusType"]:
737                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
738
739            if "assetType" in iJSON.keys() and iJSON["assetType"]:
740                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
741
742            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
743                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
744
745            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
746                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
747
748            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
749                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
750
751            if "currency" in iJSON.keys():
752                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
753
754            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
755                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
756
757            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
758                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
759
760            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
761                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
762
763            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
764                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
765
766            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
767                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
768
769            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
770                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
771
772            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
773                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
774
775            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
776                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
777
778            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
779                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
780
781            iExt = None
782            if iJSON["type"] == "Bonds":
783                info.extend([
784                    splitLine,
785                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
786                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
787                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
788                        iJSON["nominal"]["currency"],
789                    )),
790                ])
791
792                if "floatingCouponFlag" in iJSON.keys():
793                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
794
795                if "amortizationFlag" in iJSON.keys():
796                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
797
798                info.append(splitLine)
799
800                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
801                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
802
803                if iJSON["figi"]:
804                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
805
806                    info.extend([
807                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
808                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
809                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
810                    ])
811
812                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
813                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
814                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
815                        iJSON["aciValue"]["currency"]
816                    )))
817
818            if "currentPrice" in iJSON.keys():
819                info.append(splitLine)
820
821                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
822                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
823
824                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
825                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
826                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
827                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
828                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
829
830                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
831                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
832
833                info.extend([
834                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
835                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
836                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
837                    )),
838                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
839                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
840                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
841                    )),
842                    "| Changes between last deal price and last close              | {:<54} |\n".format(
843                        "{:.2f}%{}".format(
844                            iJSON["currentPrice"]["changes"],
845                            " ({}{:.2f} {})".format(
846                                "+" if bondChangesDelta > 0 else "",
847                                bondChangesDelta,
848                                aciCurrency
849                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
850                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
851                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
852                                currency
853                            ),
854                        )
855                    ),
856                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
857                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
858                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
859                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
860                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
861                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
862                    )),
863                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
864                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
865                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
866                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
867                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
868                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
869                    )),
870                ])
871
872            if "lot" in iJSON.keys():
873                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
874
875            if "step" in iJSON.keys() and iJSON["step"] != 0:
876                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
877
878            # Add bond payment calendar:
879            if iJSON["type"] == "Bonds":
880                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
881                info.extend(["\n", strCalendar])
882
883            infoText += "".join(info)
884
885            if show:
886                uLogger.info("{}".format(infoText))
887
888            else:
889                uLogger.debug("{}".format(infoText))
890
891            if self.infoFile is not None:
892                with open(self.infoFile, "w", encoding="UTF-8") as fH:
893                    fH.write(infoText)
894
895                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
896
897        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self.ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
899    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
900        """
901        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
902
903        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
904        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
905        :return: JSON formatted data with information about instrument.
906        """
907        tickerJSON = {}
908        if self.moreDebug:
909            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
910
911        if not self.ticker:
912            uLogger.warning("self.ticker variable is not be empty!")
913
914        else:
915            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
916                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
917                raise Exception("Instrument not allowed")
918
919            if not self.iList:
920                self.iList = self.Listing()
921
922            if self.ticker in self.iList["Shares"].keys():
923                tickerJSON = self.iList["Shares"][self.ticker]
924                if self.moreDebug:
925                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
926
927            elif self.ticker in self.iList["Currencies"].keys():
928                tickerJSON = self.iList["Currencies"][self.ticker]
929                if self.moreDebug:
930                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
931
932            elif self.ticker in self.iList["Bonds"].keys():
933                tickerJSON = self.iList["Bonds"][self.ticker]
934                if self.moreDebug:
935                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
936
937            elif self.ticker in self.iList["Etfs"].keys():
938                tickerJSON = self.iList["Etfs"][self.ticker]
939                if self.moreDebug:
940                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
941
942            elif self.ticker in self.iList["Futures"].keys():
943                tickerJSON = self.iList["Futures"][self.ticker]
944                if self.moreDebug:
945                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
946
947        if tickerJSON:
948            self.figi = tickerJSON["figi"]
949
950            if requestPrice:
951                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
952
953                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
954                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
955
956                else:
957                    tickerJSON["currentPrice"]["changes"] = 0
958
959            if show:
960                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
961
962        else:
963            if show:
964                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
965
966        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 968    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 969        """
 970        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 971
 972        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 973        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 974        :return: JSON formatted data with information about instrument.
 975        """
 976        figiJSON = {}
 977        if self.moreDebug:
 978            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
 979
 980        if not self.figi:
 981            uLogger.warning("self.figi variable is not be empty!")
 982
 983        else:
 984            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 985                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
 986                raise Exception("Instrument not allowed")
 987
 988            if not self.iList:
 989                self.iList = self.Listing()
 990
 991            for item in self.iList["Shares"].keys():
 992                if self.figi == self.iList["Shares"][item]["figi"]:
 993                    figiJSON = self.iList["Shares"][item]
 994
 995                    if self.moreDebug:
 996                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
 997
 998                    break
 999
1000            if not figiJSON:
1001                for item in self.iList["Currencies"].keys():
1002                    if self.figi == self.iList["Currencies"][item]["figi"]:
1003                        figiJSON = self.iList["Currencies"][item]
1004
1005                        if self.moreDebug:
1006                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1007
1008                        break
1009
1010            if not figiJSON:
1011                for item in self.iList["Bonds"].keys():
1012                    if self.figi == self.iList["Bonds"][item]["figi"]:
1013                        figiJSON = self.iList["Bonds"][item]
1014
1015                        if self.moreDebug:
1016                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1017
1018                        break
1019
1020            if not figiJSON:
1021                for item in self.iList["Etfs"].keys():
1022                    if self.figi == self.iList["Etfs"][item]["figi"]:
1023                        figiJSON = self.iList["Etfs"][item]
1024
1025                        if self.moreDebug:
1026                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1027
1028                        break
1029
1030            if not figiJSON:
1031                for item in self.iList["Futures"].keys():
1032                    if self.figi == self.iList["Futures"][item]["figi"]:
1033                        figiJSON = self.iList["Futures"][item]
1034
1035                        if self.moreDebug:
1036                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1037
1038                        break
1039
1040        if figiJSON:
1041            self.figi = figiJSON["figi"]
1042            self.ticker = figiJSON["ticker"]
1043
1044            if requestPrice:
1045                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1046
1047                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1048                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1049
1050                else:
1051                    figiJSON["currentPrice"]["changes"] = 0
1052
1053            if show:
1054                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1055
1056        else:
1057            if show:
1058                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1059
1060        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1062    def GetCurrentPrices(self, show: bool = True) -> dict:
1063        """
1064        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1065        `{"buy": [{"price": 1243.8, "quantity": 193},
1066                  {"price": 1244.0, "quantity": 168},
1067                  {"price": 1244.8, "quantity": 5},
1068                  {"price": 1245.0, "quantity": 61},
1069                  {"price": 1245.4, "quantity": 60}],
1070          "sell": [{"price": 1243.6, "quantity": 8},
1071                   {"price": 1242.6, "quantity": 10},
1072                   {"price": 1242.4, "quantity": 18},
1073                   {"price": 1242.2, "quantity": 50},
1074                   {"price": 1242.0, "quantity": 113}],
1075          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1076        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1077        - sell: list of dicts with Buyers prices,
1078            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1079            - quantity: volume value by current price in lots,
1080        - limitUp: current trade session limit price, maximum,
1081        - limitDown: current trade session limit price, minimum,
1082        - lastPrice: last deal price of the instrument,
1083        - closePrice: previous trade session close price of the instrument.
1084
1085        See also: `SearchByTicker()` and `SearchByFIGI()`.
1086        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1087        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1088
1089        :param show: if `True` then print DOM to log and console.
1090        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1091                 If an error occurred then returns an empty record:
1092                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1093        """
1094        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1095
1096        if self.depth < 1:
1097            uLogger.error("Depth of Market (DOM) must be >=1!")
1098            raise Exception("Incorrect value")
1099
1100        if not (self.ticker or self.figi):
1101            uLogger.error("self.ticker or self.figi variables must be defined!")
1102            raise Exception("Ticker or FIGI required")
1103
1104        if self.ticker and not self.figi:
1105            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1106            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1107
1108        if not self.ticker and self.figi:
1109            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1110            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1111
1112        if not self.figi:
1113            uLogger.error("FIGI is not defined!")
1114            raise Exception("Ticker or FIGI required")
1115
1116        else:
1117            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1118
1119            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1120            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1121            self.body = str({"figi": self.figi, "depth": self.depth})
1122            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1123
1124            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1125                # list of dicts with sellers orders:
1126                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1127
1128                # list of dicts with buyers orders:
1129                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1130
1131                # max price of instrument at this time:
1132                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1133
1134                # min price of instrument at this time:
1135                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1136
1137                # last price of deal with instrument:
1138                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1139
1140                # last close price of instrument:
1141                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1142
1143            else:
1144                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1145                uLogger.debug("Server response: {}".format(pricesResponse))
1146
1147            if show:
1148                if prices["buy"] or prices["sell"]:
1149                    info = [
1150                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1151                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1152                            self.ticker,
1153                            self.figi,
1154                            self.depth,
1155                        ),
1156                        "-" * 60, "\n",
1157                        "             Orders of Buyers | Orders of Sellers\n",
1158                        "-" * 60, "\n",
1159                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1160                        "-" * 60, "\n",
1161                    ]
1162
1163                    if not prices["buy"]:
1164                        info.append("                              | No orders!\n")
1165                        sumBuy = 0
1166
1167                    else:
1168                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1169                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1170                        for item in maxMinSorted:
1171                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1172
1173                    if not prices["sell"]:
1174                        info.append("No orders!                    |\n")
1175                        sumSell = 0
1176
1177                    else:
1178                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1179                        for item in prices["sell"]:
1180                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1181
1182                    info.extend([
1183                        "-" * 60, "\n",
1184                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1185                        "-" * 60, "\n",
1186                    ])
1187
1188                    infoText = "".join(info)
1189
1190                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1191
1192                else:
1193                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1194
1195        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1197    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1198        """
1199        This method get and show information about all available broker instruments for current user account.
1200        If `instrumentsFile` string is not empty then also save information to this file.
1201
1202        :param show: if `True` then print results to console, if `False` — print only to file.
1203        :return: multi-lines string with all available broker instruments
1204        """
1205        if not self.iList:
1206            self.iList = self.Listing()
1207
1208        info = [
1209            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1210            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1211        ]
1212
1213        # add instruments count by type:
1214        for iType in self.iList.keys():
1215            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1216
1217        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1218        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1219
1220        # generating info tables with all instruments by type:
1221        for iType in self.iList.keys():
1222            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1223
1224            for instrument in self.iList[iType].keys():
1225                iName = self.iList[iType][instrument]["name"]  # instrument's name
1226                if len(iName) > 57:
1227                    iName = "{}...".format(iName[:54])  # right trim for a long string
1228
1229                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1230                    self.iList[iType][instrument]["ticker"],
1231                    iName,
1232                    self.iList[iType][instrument]["figi"],
1233                    self.iList[iType][instrument]["currency"],
1234                    self.iList[iType][instrument]["lot"],
1235                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1236                ))
1237
1238        infoText = "".join(info)
1239
1240        if show:
1241            uLogger.info(infoText)
1242
1243        if self.instrumentsFile:
1244            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1245                fH.write(infoText)
1246
1247            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1248
1249        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1251    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1252        """
1253        This method search and show information about instruments by part of its ticker, FIGI or name.
1254        If `searchResultsFile` string is not empty then also save information to this file.
1255
1256        :param pattern: string with part of ticker, FIGI or instrument's name.
1257        :param show: if `True` then print results to console, if `False` — return list of result only.
1258        :return: list of dictionaries with all found instruments.
1259        """
1260        if not self.iList:
1261            self.iList = self.Listing()
1262
1263        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1264        compiledPattern = re.compile(pattern, re.IGNORECASE)
1265
1266        for iType in self.iList:
1267            for instrument in self.iList[iType].values():
1268                searchResult = compiledPattern.search(" ".join(
1269                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1270                ))
1271
1272                if searchResult:
1273                    searchResults[iType][instrument["ticker"]] = instrument
1274
1275        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1276        info = [
1277            "# Search results\n\n",
1278            "* **Search pattern:** [{}]\n".format(pattern),
1279            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1280            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1281        ]
1282        infoShort = info[:]
1283
1284        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1285        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1286        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1287
1288        if resultsLen == 0:
1289            info.append("\nNo results\n")
1290            infoShort.append("\nNo results\n")
1291            uLogger.warning("No results. Try changing your search pattern.")
1292
1293        else:
1294            for iType in searchResults:
1295                iTypeValuesCount = len(searchResults[iType].values())
1296                if iTypeValuesCount > 0:
1297                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1298                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1299
1300                    for instrument in searchResults[iType].values():
1301                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1302                            instrument["type"],
1303                            instrument["ticker"],
1304                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1305                            instrument["figi"],
1306                        ))
1307
1308                    if iTypeValuesCount <= 5:
1309                        infoShort.extend(info[-iTypeValuesCount:])
1310
1311                    else:
1312                        infoShort.extend(info[-5:])
1313                        infoShort.append(skippedLine)
1314
1315        infoText = "".join(info)
1316        infoTextShort = "".join(infoShort)
1317
1318        if show:
1319            uLogger.info(infoTextShort)
1320            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1321
1322        if self.searchResultsFile:
1323            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1324                fH.write(infoText)
1325
1326            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1327
1328        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1330    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1331        """
1332        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1333
1334        :param instruments: list of strings with tickers or FIGIs.
1335        :return: list with unique instrument FIGIs only.
1336        """
1337        requestedInstruments = []
1338        for iName in instruments:
1339            if iName not in self.aliases.keys():
1340                if iName not in requestedInstruments:
1341                    requestedInstruments.append(iName)
1342
1343            else:
1344                if iName not in requestedInstruments:
1345                    if self.aliases[iName] not in requestedInstruments:
1346                        requestedInstruments.append(self.aliases[iName])
1347
1348        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1349
1350        onlyUniqueFIGIs = []
1351        for iName in requestedInstruments:
1352            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1353                continue
1354
1355            self.ticker = iName
1356            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1357
1358            if not iData:
1359                self.ticker = ""
1360                self.figi = iName
1361
1362                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1363
1364                if not iData:
1365                    self.figi = ""
1366                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1367
1368            if iData and iData["figi"] not in onlyUniqueFIGIs:
1369                onlyUniqueFIGIs.append(iData["figi"])
1370
1371        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1372
1373        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1375    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1376        """
1377        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1378
1379        See limits: https://tinkoff.github.io/investAPI/limits/
1380
1381        If `pricesFile` string is not empty then also save information to this file.
1382
1383        :param instruments: list of strings with tickers or FIGIs.
1384        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1385        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1386                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1387        """
1388        if instruments is None or not instruments:
1389            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1390            raise Exception("Ticker or FIGI required")
1391
1392        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1393
1394        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1395
1396        iList = []  # trying to get info and current prices about all unique instruments:
1397        for self.figi in onlyUniqueFIGIs:
1398            iData = self.SearchByFIGI(requestPrice=True)
1399            iList.append(iData)
1400
1401        self.ShowListOfPrices(iList, show)
1402
1403        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1405    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1406        """
1407        Show table contains current prices of given instruments.
1408
1409        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1410                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1411        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1412        :return: multilines text in Markdown format as a table contains current prices.
1413        """
1414        infoText = ""
1415
1416        if show or self.pricesFile:
1417            info = [
1418                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1419                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1420                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1421            ]
1422
1423            for item in iList:
1424                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1425                    item["ticker"],
1426                    item["figi"],
1427                    item["type"],
1428                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1429                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1430                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1431                    "{} / {}".format(
1432                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1433                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1434                    ),
1435                    "{} / {}".format(
1436                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1437                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1438                    ),
1439                    item["currency"],
1440                ))
1441
1442            infoText = "".join(info)
1443
1444            if show:
1445                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1446
1447            if self.pricesFile:
1448                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1449                    fH.write(infoText)
1450
1451                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1452
1453        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1455    def RequestTradingStatus(self) -> dict:
1456        """
1457        Requesting trading status for the instrument defined by `figi` variable.
1458
1459        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1460
1461        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1462
1463        :return: dictionary with trading status attributes. Response example:
1464                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1465                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1466        """
1467        if self.figi is None or not self.figi:
1468            uLogger.error("Variable `figi` must be defined for using this method!")
1469            raise Exception("FIGI required")
1470
1471        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1472
1473        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1474        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1475        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1476
1477        if self.moreDebug:
1478            uLogger.debug("Records about current trading status successfully received")
1479
1480        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1482    def RequestPortfolio(self) -> dict:
1483        """
1484        Requesting actual user's portfolio for current `accountId`.
1485
1486        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1487
1488        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1489
1490        :return: dictionary with user's portfolio.
1491        """
1492        if self.accountId is None or not self.accountId:
1493            uLogger.error("Variable `accountId` must be defined for using this method!")
1494            raise Exception("Account ID required")
1495
1496        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1497
1498        self.body = str({"accountId": self.accountId})
1499        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1500        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1501
1502        if self.moreDebug:
1503            uLogger.debug("Records about user's portfolio successfully received")
1504
1505        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1507    def RequestPositions(self) -> dict:
1508        """
1509        Requesting open positions by currencies and instruments for current `accountId`.
1510
1511        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1512
1513        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1514
1515        :return: dictionary with open positions by instruments.
1516        """
1517        if self.accountId is None or not self.accountId:
1518            uLogger.error("Variable `accountId` must be defined for using this method!")
1519            raise Exception("Account ID required")
1520
1521        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1522
1523        self.body = str({"accountId": self.accountId})
1524        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1525        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1526
1527        if self.moreDebug:
1528            uLogger.debug("Records about current open positions successfully received")
1529
1530        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1532    def RequestPendingOrders(self) -> list:
1533        """
1534        Requesting current actual pending orders for current `accountId`.
1535
1536        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1537
1538        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1539
1540        :return: list of dictionaries with pending orders.
1541        """
1542        if self.accountId is None or not self.accountId:
1543            uLogger.error("Variable `accountId` must be defined for using this method!")
1544            raise Exception("Account ID required")
1545
1546        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1547
1548        self.body = str({"accountId": self.accountId})
1549        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1550        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1551
1552        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1553
1554        return rawOrders

Requesting current actual pending orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending orders.

def RequestStopOrders(self) -> list:
1556    def RequestStopOrders(self) -> list:
1557        """
1558        Requesting current actual stop orders for current `accountId`.
1559
1560        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1561
1562        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1563
1564        :return: list of dictionaries with stop orders.
1565        """
1566        if self.accountId is None or not self.accountId:
1567            uLogger.error("Variable `accountId` must be defined for using this method!")
1568            raise Exception("Account ID required")
1569
1570        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1571
1572        self.body = str({"accountId": self.accountId})
1573        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1574        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1575
1576        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1577
1578        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1580    def Overview(self, show: bool = False, details: str = "full") -> dict:
1581        """
1582        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1583        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1584        and `overviewBondsCalendarFile` are defined then also save information to file.
1585
1586        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1587        many requests about the state of the portfolio, and then, based on the received data, a large number
1588        of calculation and statistics are collected.
1589
1590        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1591        :param details: how detailed should the information be?
1592        - `full` — shows full available information about portfolio status (by default),
1593        - `positions` — shows only open positions,
1594        - `orders` — shows only sections of open limits and stop orders.
1595        - `digest` — show a short digest of the portfolio status,
1596        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1597        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1598        :return: dictionary with client's raw portfolio and some statistics.
1599        """
1600        if self.accountId is None or not self.accountId:
1601            uLogger.error("Variable `accountId` must be defined for using this method!")
1602            raise Exception("Account ID required")
1603
1604        view = {
1605            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1606                "headers": {},  # list of dictionaries, response headers without "positions" section
1607                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1608                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1609                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1610                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1611                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1612                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1613                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1614                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1615                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1616            },
1617            "stat": {  # --- some statistics calculated using "raw" sections:
1618                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1619                "availableRUB": 0.,  # available rubles (without other currencies)
1620                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1621                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1622                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1623                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1624                "sharesCostRUB": 0.,  # costs of all shares in RUB
1625                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1626                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1627                "futuresCostRUB": 0.,  # costs of all futures in RUB
1628                "Currencies": [],  # list of dictionaries of all currencies statistics
1629                "Shares": [],  # list of dictionaries of all shares statistics
1630                "Bonds": [],  # list of dictionaries of all bonds statistics
1631                "Etfs": [],  # list of dictionaries of all etfs statistics
1632                "Futures": [],  # list of dictionaries of all futures statistics
1633                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1634                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1635                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1636                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1637                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1638            },
1639            "analytics": {  # --- some analytics of portfolio:
1640                "distrByAssets": {},  # portfolio distribution by assets
1641                "distrByCompanies": {},  # portfolio distribution by companies
1642                "distrBySectors": {},  # portfolio distribution by sectors
1643                "distrByCurrencies": {},  # portfolio distribution by currencies
1644                "distrByCountries": {},  # portfolio distribution by countries
1645                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1646            }
1647        }
1648
1649        details = details.lower()
1650        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1651        if details not in availableDetails:
1652            details = "full"
1653            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1654
1655        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1656
1657        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1658        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1659        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1660        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1661
1662        # save response headers without "positions" section:
1663        for key in portfolioResponse.keys():
1664            if key != "positions":
1665                view["raw"]["headers"][key] = portfolioResponse[key]
1666
1667            else:
1668                continue
1669
1670        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1671        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1672        for item in portfolioResponse["positions"]:
1673            if item["instrumentType"] == "currency":
1674                self.figi = item["figi"]
1675                curr = self.SearchByFIGI(requestPrice=False)
1676
1677                # current price of currency in RUB:
1678                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1679                    "name": curr["name"],
1680                    "currentPrice": NanoToFloat(
1681                        item["currentPrice"]["units"],
1682                        item["currentPrice"]["nano"]
1683                    ),
1684                }
1685
1686                view["raw"]["Currencies"].append(item)
1687
1688            elif item["instrumentType"] == "share":
1689                view["raw"]["Shares"].append(item)
1690
1691            elif item["instrumentType"] == "bond":
1692                view["raw"]["Bonds"].append(item)
1693
1694            elif item["instrumentType"] == "etf":
1695                view["raw"]["Etfs"].append(item)
1696
1697            elif item["instrumentType"] == "futures":
1698                view["raw"]["Futures"].append(item)
1699
1700            else:
1701                continue
1702
1703        # how many volume of currencies (by ISO currency name) are blocked:
1704        for item in view["raw"]["positions"]["blocked"]:
1705            blocked = NanoToFloat(item["units"], item["nano"])
1706            if blocked > 0:
1707                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1708
1709        # how many volume of instruments (by FIGI) are blocked:
1710        for item in view["raw"]["positions"]["securities"]:
1711            blocked = int(item["blocked"])
1712            if blocked > 0:
1713                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1714
1715        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1716
1717        if "rub" in allBlocked.keys():
1718            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1719
1720        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1721        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1722        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1723        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1724        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1725        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1726        view["stat"]["portfolioCostRUB"] = sum([
1727            view["stat"]["allCurrenciesCostRUB"],
1728            view["stat"]["sharesCostRUB"],
1729            view["stat"]["bondsCostRUB"],
1730            view["stat"]["etfsCostRUB"],
1731            view["stat"]["futuresCostRUB"],
1732        ])
1733
1734        # --- calculating some portfolio statistics:
1735        byComp = {}  # distribution by companies
1736        bySect = {}  # distribution by sectors
1737        byCurr = {}  # distribution by currencies (include RUB)
1738        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1739        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1740
1741        for item in portfolioResponse["positions"]:
1742            self.figi = item["figi"]
1743            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1744
1745            if instrument:
1746                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1747                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1748
1749                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1750                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1751
1752                else:
1753                    blocked = 0
1754
1755                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1756                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1757                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1758                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1759                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1760                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1761                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1762                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1763                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1764                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1765                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1766                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1767
1768                statData = {
1769                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1770                    "ticker": instrument["ticker"],  # ticker by FIGI
1771                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1772                    "volume": volume,  # available volume of instrument
1773                    "lots": lots,  # volume in lots of instrument
1774                    "direction": direction,  # direction of an instrument's position: short or long
1775                    "blocked": blocked,  # blocked volume of currency or instrument
1776                    "currentPrice": curPrice,  # current instrument's price in basic asset
1777                    "average": average,  # current average position price
1778                    "cost": cost,  # current cost of all volume of instrument in basic asset
1779                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1780                    "costRUB": costRUB,  # cost of instrument in ruble
1781                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1782                    "profit": profit,  # expected profit at current moment
1783                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1784                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1785                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1786                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1787                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1788                    "step": instrument["step"],  # minimum price increment
1789                }
1790
1791                # adding distribution by unique countries:
1792                if statData["country"] not in byCountry.keys():
1793                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1794
1795                else:
1796                    byCountry[statData["country"]]["cost"] += costRUB
1797                    byCountry[statData["country"]]["percent"] += percentCostRUB
1798
1799                if item["instrumentType"] != "currency":
1800                    # adding distribution by unique companies:
1801                    if statData["name"]:
1802                        if statData["name"] not in byComp.keys():
1803                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1804
1805                        else:
1806                            byComp[statData["name"]]["cost"] += costRUB
1807                            byComp[statData["name"]]["percent"] += percentCostRUB
1808
1809                    # adding distribution by unique sectors:
1810                    if statData["sector"] not in bySect.keys():
1811                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1812
1813                    else:
1814                        bySect[statData["sector"]]["cost"] += costRUB
1815                        bySect[statData["sector"]]["percent"] += percentCostRUB
1816
1817                # adding distribution by unique currencies:
1818                if currency not in byCurr.keys():
1819                    byCurr[currency] = {
1820                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1821                        "cost": costRUB,
1822                        "percent": percentCostRUB
1823                    }
1824
1825                else:
1826                    byCurr[currency]["cost"] += costRUB
1827                    byCurr[currency]["percent"] += percentCostRUB
1828
1829                # saving statistics for every instrument:
1830                if item["instrumentType"] == "currency":
1831                    view["stat"]["Currencies"].append(statData)
1832
1833                    # update dict with free funds for trading (total - blocked) by currencies
1834                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1835                    view["stat"]["funds"][currency] = {
1836                        "total": volume,
1837                        "totalCostRUB": costRUB,  # total volume cost in rubles
1838                        "free": volume - blocked,
1839                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1840                    }
1841
1842                elif item["instrumentType"] == "share":
1843                    view["stat"]["Shares"].append(statData)
1844
1845                elif item["instrumentType"] == "bond":
1846                    view["stat"]["Bonds"].append(statData)
1847
1848                elif item["instrumentType"] == "etf":
1849                    view["stat"]["Etfs"].append(statData)
1850
1851                elif item["instrumentType"] == "Futures":
1852                    view["stat"]["Futures"].append(statData)
1853
1854                else:
1855                    continue
1856
1857        # total changes in Russian Ruble:
1858        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1859        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1860        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1861        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1862        view["stat"]["funds"]["rub"] = {
1863            "total": view["stat"]["availableRUB"],
1864            "totalCostRUB": view["stat"]["availableRUB"],
1865            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1866            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1867        }
1868
1869        # --- pending orders sector data:
1870        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1871        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1872
1873        for item in view["raw"]["orders"]:
1874            self.figi = item["figi"]
1875
1876            if item["figi"] not in uniquePendingOrdersFIGIs:
1877                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1878
1879                uniquePendingOrdersFIGIs.append(item["figi"])
1880                uniquePendingOrders[item["figi"]] = instrument
1881
1882            else:
1883                instrument = uniquePendingOrders[item["figi"]]
1884
1885            if instrument:
1886                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1887                orderType = TKS_ORDER_TYPES[item["orderType"]]
1888                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1889                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1890
1891                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1892                if item["direction"] == "ORDER_DIRECTION_BUY":
1893                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1894
1895                else:
1896                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1897
1898                # requested price for order execution:
1899                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1900
1901                # necessary changes in percent to reach target from current price:
1902                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1903
1904                view["stat"]["orders"].append({
1905                    "orderID": item["orderId"],  # orderId number parameter of current order
1906                    "figi": item["figi"],  # FIGI identification
1907                    "ticker": instrument["ticker"],  # ticker name by FIGI
1908                    "lotsRequested": item["lotsRequested"],  # requested lots value
1909                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1910                    "currentPrice": lastPrice,  # current instrument's price for defined action
1911                    "targetPrice": target,  # requested price for order execution in base currency
1912                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1913                    "percentChanges": changes,  # changes in percent to target from current price
1914                    "currency": item["currency"],  # instrument's currency name
1915                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1916                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1917                    "status": orderState,  # order status from TKS_ORDER_STATES
1918                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1919                })
1920
1921        # --- stop orders sector data:
1922        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1923        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1924
1925        for item in view["raw"]["stopOrders"]:
1926            self.figi = item["figi"]
1927
1928            if item["figi"] not in uniqueStopOrdersFIGIs:
1929                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1930
1931                uniqueStopOrdersFIGIs.append(item["figi"])
1932                uniqueStopOrders[item["figi"]] = instrument
1933
1934            else:
1935                instrument = uniqueStopOrders[item["figi"]]
1936
1937            if instrument:
1938                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1939                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1940                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1941
1942                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1943                if "expirationTime" in item.keys():
1944                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1945                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1946
1947                else:
1948                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1949                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1950
1951                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1952                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1953                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1954
1955                else:
1956                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1957
1958                # requested price when stop-order executed:
1959                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1960
1961                # price for limit-order, set up when stop-order executed:
1962                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1963
1964                # necessary changes in percent to reach target from current price:
1965                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1966
1967                view["stat"]["stopOrders"].append({
1968                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1969                    "figi": item["figi"],  # FIGI identification
1970                    "ticker": instrument["ticker"],  # ticker name by FIGI
1971                    "lotsRequested": item["lotsRequested"],  # requested lots value
1972                    "currentPrice": lastPrice,  # current instrument's price for defined action
1973                    "targetPrice": target,  # requested price for stop-order execution in base currency
1974                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1975                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1976                    "percentChanges": changes,  # changes in percent to target from current price
1977                    "currency": item["currency"],  # instrument's currency name
1978                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1979                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1980                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1981                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1982                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1983                })
1984
1985        # --- calculating data for analytics section:
1986        # portfolio distribution by assets:
1987        view["analytics"]["distrByAssets"] = {
1988            "Ruble": {
1989                "uniques": 1,
1990                "cost": view["stat"]["availableRUB"],
1991                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1992            },
1993            "Currencies": {
1994                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1995                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1996                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1997            },
1998            "Shares": {
1999                "uniques": len(view["stat"]["Shares"]),
2000                "cost": view["stat"]["sharesCostRUB"],
2001                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2002            },
2003            "Bonds": {
2004                "uniques": len(view["stat"]["Bonds"]),
2005                "cost": view["stat"]["bondsCostRUB"],
2006                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2007            },
2008            "Etfs": {
2009                "uniques": len(view["stat"]["Etfs"]),
2010                "cost": view["stat"]["etfsCostRUB"],
2011                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2012            },
2013            "Futures": {
2014                "uniques": len(view["stat"]["Futures"]),
2015                "cost": view["stat"]["futuresCostRUB"],
2016                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2017            },
2018        }
2019
2020        # portfolio distribution by companies:
2021        view["analytics"]["distrByCompanies"]["All money cash"] = {
2022            "ticker": "",
2023            "cost": view["stat"]["allCurrenciesCostRUB"],
2024            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2025        }
2026        view["analytics"]["distrByCompanies"].update(byComp)
2027
2028        # portfolio distribution by sectors:
2029        view["analytics"]["distrBySectors"]["All money cash"] = {
2030            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2031            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2032        }
2033        view["analytics"]["distrBySectors"].update(bySect)
2034
2035        # portfolio distribution by currencies:
2036        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2037            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2038
2039            if self.moreDebug:
2040                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2041
2042        view["analytics"]["distrByCurrencies"].update(byCurr)
2043        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2044        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2045
2046        # portfolio distribution by countries:
2047        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2048            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2049
2050            if self.moreDebug:
2051                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2052
2053        view["analytics"]["distrByCountries"].update(byCountry)
2054        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2055        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2056
2057        # --- Prepare text statistics overview in human-readable:
2058        if show:
2059            # Whatever the value `details`, header not changes:
2060            info = [
2061                "# Client's portfolio\n\n",
2062                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2063                "* **Account ID:** [{}]\n".format(self.accountId),
2064            ]
2065
2066            if details in ["full", "positions", "digest"]:
2067                info.extend([
2068                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2069                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2070                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2071                        view["stat"]["totalChangesRUB"],
2072                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2073                        view["stat"]["totalChangesPercentRUB"],
2074                    ),
2075                ])
2076
2077            if details in ["full", "positions"]:
2078                info.extend([
2079                    "## Open positions\n\n",
2080                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2081                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2082                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2083                        "{:.2f} ({:.2f}) rub".format(
2084                            view["stat"]["availableRUB"],
2085                            view["stat"]["blockedRUB"],
2086                        )
2087                    )
2088                ])
2089
2090                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2091                    return [
2092                        "|                             |                                 |          |              |              |                     |                              |\n",
2093                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2094                            noTradeStr if noTradeStr else typeStr,
2095                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2096                        ),
2097                    ]
2098
2099                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2100                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2101                        "{} [{}]".format(data["ticker"], data["figi"]),
2102                        "{:.2f} ({:.2f}) {}".format(
2103                            data["volume"],
2104                            data["blocked"],
2105                            data["currency"],
2106                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2107                            data["volume"],
2108                            data["blocked"],
2109                        ),
2110                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2111                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2112                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2113                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2114                        "{}{:.2f} {} ({}{:.2f}%)".format(
2115                            "+" if data["profit"] > 0 else "",
2116                            data["profit"], data["baseCurrencyName"],
2117                            "+" if data["percentProfit"] > 0 else "",
2118                            data["percentProfit"],
2119                        ),
2120                    )
2121
2122                # --- Show currencies section:
2123                if view["stat"]["Currencies"]:
2124                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2125                    for item in view["stat"]["Currencies"]:
2126                        info.append(_InfoStr(item, showCurrencyName=True))
2127
2128                else:
2129                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2130
2131                # --- Show shares section:
2132                if view["stat"]["Shares"]:
2133                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2134
2135                    for item in view["stat"]["Shares"]:
2136                        info.append(_InfoStr(item))
2137
2138                else:
2139                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2140
2141                # --- Show bonds section:
2142                if view["stat"]["Bonds"]:
2143                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2144
2145                    for item in view["stat"]["Bonds"]:
2146                        info.append(_InfoStr(item))
2147
2148                else:
2149                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2150
2151                # --- Show etfs section:
2152                if view["stat"]["Etfs"]:
2153                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2154
2155                    for item in view["stat"]["Etfs"]:
2156                        info.append(_InfoStr(item))
2157
2158                else:
2159                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2160
2161                # --- Show futures section:
2162                if view["stat"]["Futures"]:
2163                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2164
2165                    for item in view["stat"]["Futures"]:
2166                        info.append(_InfoStr(item))
2167
2168                else:
2169                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2170
2171            if details in ["full", "orders"]:
2172                # --- Show pending orders section:
2173                if view["stat"]["orders"]:
2174                    info.extend([
2175                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2176                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2177                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2178                    ])
2179
2180                    for item in view["stat"]["orders"]:
2181                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2182                            "{} [{}]".format(item["ticker"], item["figi"]),
2183                            item["orderID"],
2184                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2185                            "{} {} ({}{:.2f}%)".format(
2186                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2187                                item["baseCurrencyName"],
2188                                "+" if item["percentChanges"] > 0 else "",
2189                                float(item["percentChanges"]),
2190                            ),
2191                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2192                            item["action"],
2193                            item["type"],
2194                            item["date"],
2195                        ))
2196
2197                else:
2198                    info.append("\n## Total pending limit-orders: 0\n")
2199
2200                # --- Show stop orders section:
2201                if view["stat"]["stopOrders"]:
2202                    info.extend([
2203                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2204                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2205                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2206                    ])
2207
2208                    for item in view["stat"]["stopOrders"]:
2209                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2210                            "{} [{}]".format(item["ticker"], item["figi"]),
2211                            item["orderID"],
2212                            item["lotsRequested"],
2213                            "{} {} ({}{:.2f}%)".format(
2214                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2215                                item["baseCurrencyName"],
2216                                "+" if item["percentChanges"] > 0 else "",
2217                                float(item["percentChanges"]),
2218                            ),
2219                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2220                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2221                            item["action"],
2222                            item["type"],
2223                            item["expType"],
2224                            item["createDate"],
2225                            item["expDate"],
2226                        ))
2227
2228                else:
2229                    info.append("\n## Total stop-orders: 0\n")
2230
2231            if details in ["full", "analytics"]:
2232                # -- Show analytics section:
2233                if view["stat"]["portfolioCostRUB"] > 0:
2234                    info.extend([
2235                        "\n# Analytics\n"
2236                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2237                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2238                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2239                            view["stat"]["totalChangesRUB"],
2240                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2241                            view["stat"]["totalChangesPercentRUB"],
2242                        ),
2243                        "\n## Portfolio distribution by assets\n"
2244                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2245                        "|------------------------------------|---------|---------|--------------------|\n",
2246                    ])
2247
2248                    for key in view["analytics"]["distrByAssets"].keys():
2249                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2250                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2251                                key,
2252                                view["analytics"]["distrByAssets"][key]["uniques"],
2253                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2254                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2255                            ))
2256
2257                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2258
2259                    info.extend([
2260                        "\n## Portfolio distribution by companies\n"
2261                        "\n| Company                                      | Percent | Current cost       |\n",
2262                        aSepLine,
2263                    ])
2264
2265                    for company in view["analytics"]["distrByCompanies"].keys():
2266                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2267                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2268                                "{}{}".format(
2269                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2270                                    company,
2271                                ),
2272                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2273                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2274                            ))
2275
2276                    info.extend([
2277                        "\n## Portfolio distribution by sectors\n"
2278                        "\n| Sector                                       | Percent | Current cost       |\n",
2279                        aSepLine,
2280                    ])
2281
2282                    for sector in view["analytics"]["distrBySectors"].keys():
2283                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2284                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2285                                sector,
2286                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2287                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2288                            ))
2289
2290                    info.extend([
2291                        "\n## Portfolio distribution by currencies\n"
2292                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2293                        aSepLine,
2294                    ])
2295
2296                    for curr in view["analytics"]["distrByCurrencies"].keys():
2297                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2298                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2299                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2300                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2301                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2302                            ))
2303
2304                    info.extend([
2305                        "\n## Portfolio distribution by countries\n"
2306                        "\n| Assets by country                            | Percent | Current cost       |\n",
2307                        aSepLine,
2308                    ])
2309
2310                    for country in view["analytics"]["distrByCountries"].keys():
2311                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2312                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2313                                country,
2314                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2315                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2316                            ))
2317
2318            if details in ["full", "calendar"]:
2319                # -- Show bonds payment calendar section:
2320                if view["stat"]["Bonds"]:
2321                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2322                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2323                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2324
2325                else:
2326                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2327
2328            infoText = "".join(info)
2329
2330            uLogger.info(infoText)
2331
2332            if details == "full" and self.overviewFile:
2333                filename = self.overviewFile
2334
2335            elif details == "digest" and self.overviewDigestFile:
2336                filename = self.overviewDigestFile
2337
2338            elif details == "positions" and self.overviewPositionsFile:
2339                filename = self.overviewPositionsFile
2340
2341            elif details == "orders" and self.overviewOrdersFile:
2342                filename = self.overviewOrdersFile
2343
2344            elif details == "analytics" and self.overviewAnalyticsFile:
2345                filename = self.overviewAnalyticsFile
2346
2347            elif details == "calendar" and self.overviewBondsCalendarFile:
2348                filename = self.overviewBondsCalendarFile
2349
2350            else:
2351                filename = ""
2352
2353            if filename:
2354                with open(filename, "w", encoding="UTF-8") as fH:
2355                    fH.write(infoText)
2356
2357                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2358
2359        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio),
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2361    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2362        """
2363        Returns history operations between two given dates for current `accountId`.
2364        If `reportFile` string is not empty then also save human-readable report.
2365        Shows some statistical data of closed positions.
2366
2367        :param start: see docstring in `GetDatesAsString()` method
2368        :param end: see docstring in `GetDatesAsString()` method
2369        :param show: if `True` then also prints all records to the console.
2370        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2371        :return: original list of dictionaries with history of deals records from API ("operations" key):
2372                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2373                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2374        """
2375        if self.accountId is None or not self.accountId:
2376            uLogger.error("Variable `accountId` must be defined for using this method!")
2377            raise Exception("Account ID required")
2378
2379        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2380
2381        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2382
2383        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2384        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2385        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2386        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2387        customStat = {}  # custom statistics in additional to responseJSON
2388
2389        # --- output report in human-readable format:
2390        if show or self.reportFile:
2391            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2392            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2393            nextDay = ""
2394
2395            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2396
2397            if len(ops) > 0:
2398                customStat = {
2399                    "opsCount": 0,  # total operations count
2400                    "buyCount": 0,  # buy operations
2401                    "sellCount": 0,  # sell operations
2402                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2403                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2404                    "payIn": {"rub": 0.},  # Deposit brokerage account
2405                    "payOut": {"rub": 0.},  # Withdrawals
2406                    "divs": {"rub": 0.},  # Dividends income
2407                    "coupons": {"rub": 0.},  # Coupon's income
2408                    "brokerCom": {"rub": 0.},  # Service commissions
2409                    "serviceCom": {"rub": 0.},  # Service commissions
2410                    "marginCom": {"rub": 0.},  # Margin commissions
2411                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2412                }
2413
2414                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2415                for item in ops:
2416                    if item["state"] == "OPERATION_STATE_EXECUTED":
2417                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2418
2419                        # count buy operations:
2420                        if "_BUY" in item["operationType"]:
2421                            customStat["buyCount"] += 1
2422
2423                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2424                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2425
2426                            else:
2427                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2428
2429                        # count sell operations:
2430                        elif "_SELL" in item["operationType"]:
2431                            customStat["sellCount"] += 1
2432
2433                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2434                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2435
2436                            else:
2437                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2438
2439                        # count incoming operations:
2440                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2441                            if item["payment"]["currency"] in customStat["payIn"].keys():
2442                                customStat["payIn"][item["payment"]["currency"]] += payment
2443
2444                            else:
2445                                customStat["payIn"][item["payment"]["currency"]] = payment
2446
2447                        # count withdrawals operations:
2448                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2449                            if item["payment"]["currency"] in customStat["payOut"].keys():
2450                                customStat["payOut"][item["payment"]["currency"]] += payment
2451
2452                            else:
2453                                customStat["payOut"][item["payment"]["currency"]] = payment
2454
2455                        # count dividends income:
2456                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2457                            if item["payment"]["currency"] in customStat["divs"].keys():
2458                                customStat["divs"][item["payment"]["currency"]] += payment
2459
2460                            else:
2461                                customStat["divs"][item["payment"]["currency"]] = payment
2462
2463                        # count coupon's income:
2464                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2465                            if item["payment"]["currency"] in customStat["coupons"].keys():
2466                                customStat["coupons"][item["payment"]["currency"]] += payment
2467
2468                            else:
2469                                customStat["coupons"][item["payment"]["currency"]] = payment
2470
2471                        # count broker commissions:
2472                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2473                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2474                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2475
2476                            else:
2477                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2478
2479                        # count service commissions:
2480                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2481                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2482                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2483
2484                            else:
2485                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2486
2487                        # count margin commissions:
2488                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2489                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2490                                customStat["marginCom"][item["payment"]["currency"]] += payment
2491
2492                            else:
2493                                customStat["marginCom"][item["payment"]["currency"]] = payment
2494
2495                        # count withholding taxes:
2496                        elif "_TAX" in item["operationType"]:
2497                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2498                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2499
2500                            else:
2501                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2502
2503                        else:
2504                            continue
2505
2506                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2507
2508                # --- view "Actions" lines:
2509                info.extend([
2510                    "| Report sections            |                               |                              |                      |                        |\n",
2511                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2512                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2513                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2514                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2515                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2516                    ),
2517                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2518                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2519                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2520                    ),
2521                ])
2522
2523                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2524                for key in opsKeys:
2525                    if key == "rub":
2526                        continue
2527
2528                    info.extend([
2529                        "|                            |                               | {:<28} |                      |                        |\n".format(
2530                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2531                        ),
2532                        "|                            |                               | {:<28} |                      |                        |\n".format(
2533                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2534                        ),
2535                    ])
2536
2537                info.append(splitLine1)
2538
2539                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2540                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2541                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2542                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2543                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2544                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2545                    )
2546
2547                # --- view "Payments" lines:
2548                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2549                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2550
2551                for key in paymentsKeys:
2552                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2553
2554                info.append(splitLine1)
2555
2556                # --- view "Commissions and taxes" lines:
2557                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2558                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2559
2560                for key in comKeys:
2561                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2562
2563                info.append(splitLine1)
2564
2565                info.extend([
2566                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2567                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2568                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2569                ])
2570
2571            else:
2572                info.append("Broker returned no operations during this period\n")
2573
2574            # --- view "Operations" section:
2575            for item in ops:
2576                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2577                    continue
2578
2579                else:
2580                    self.figi = item["figi"] if item["figi"] else ""
2581                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2582                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2583
2584                    # group of deals during one day:
2585                    if nextDay and item["date"].split("T")[0] != nextDay:
2586                        info.append(splitLine2)
2587                        nextDay = ""
2588
2589                    else:
2590                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2591
2592                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2593                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2594                        self.figi if self.figi else "—",
2595                        instrument["ticker"] if instrument else "—",
2596                        instrument["type"] if instrument else "—",
2597                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2598                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2599                        TKS_OPERATION_STATES[item["state"]],
2600                        TKS_OPERATION_TYPES[item["operationType"]],
2601                    ))
2602
2603            infoText = "".join(info)
2604
2605            if show:
2606                if self.moreDebug:
2607                    uLogger.debug("Records about history of a client's operations successfully received")
2608
2609                uLogger.info(infoText)
2610
2611            if self.reportFile:
2612                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2613                    fH.write(infoText)
2614
2615                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2616
2617        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in GetDatesAsString() method
  • end: see docstring in GetDatesAsString() method
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2619    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2620        """
2621        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2622
2623        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2624        Warning! Broker server used ISO UTC time by default.
2625
2626        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2627        Also, `historyFile` used to update history with `onlyMissing` parameter.
2628
2629        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2630
2631        :param start: see docstring in `GetDatesAsString()` method.
2632        :param end: see docstring in `GetDatesAsString()` method.
2633        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2634                         `"hour"`, `"day"`. Default: `"hour"`.
2635        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2636                            False by default. Warning! History appends only from last candle to current time
2637                            with always update last candle!
2638        :param csvSep: separator if csv-file is used, `,` by default.
2639        :param show: if `True` then also prints Pandas DataFrame to the console.
2640        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2641                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2642        """
2643        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2644        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2645        history = None  # empty pandas object for history
2646
2647        if interval not in TKS_CANDLE_INTERVALS.keys():
2648            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2649            raise Exception("Incorrect value")
2650
2651        if not (self.ticker or self.figi):
2652            uLogger.error("Ticker or FIGI must be defined!")
2653            raise Exception("Ticker or FIGI required")
2654
2655        if self.ticker and not self.figi:
2656            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2657            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2658
2659        if self.figi and not self.ticker:
2660            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2661            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2662
2663        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2664        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2665        if interval.lower() != "day":
2666            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2667
2668        delta = dtEnd - dtStart  # current UTC time minus last time in file
2669        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2670
2671        # calculate history length in candles:
2672        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2673        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2674            length += 1  # to avoid fraction time
2675
2676        # calculate data blocks count:
2677        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2678
2679        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2680        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2681        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2682        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2683        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2684
2685        tempOld = None  # pandas object for old history, if --only-missing key present
2686        lastTime = None  # datetime object of last old candle in file
2687
2688        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2689            uLogger.debug("--only-missing key present, add only last missing candles...")
2690            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2691
2692            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2693
2694            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2695            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2696            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2697            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2698
2699            # get last datetime object from last string in file or minus 1 delta if file is empty:
2700            if len(tempOld) > 0:
2701                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2702
2703            else:
2704                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2705
2706            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2707
2708        responseJSONs = []  # raw history blocks of data
2709
2710        blockEnd = dtEnd
2711        for item in range(blocks):
2712            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2713            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2714
2715            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2716                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2717            ))
2718
2719            if blockStart == blockEnd:
2720                uLogger.debug("Skipped this zero-length block...")
2721
2722            else:
2723                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2724                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2725                self.body = str({
2726                    "figi": self.figi,
2727                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2728                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2729                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2730                })
2731                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2732
2733                if "code" in responseJSON.keys():
2734                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2735
2736                else:
2737                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2738                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2739
2740                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2741
2742            blockEnd = blockStart
2743
2744        printCount = len(responseJSONs)  # candles to show in console
2745        if responseJSONs:
2746            tempHistory = pd.DataFrame(
2747                data={
2748                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2749                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2750                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2751                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2752                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2753                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2754                    "volume": [int(item["volume"]) for item in responseJSONs],
2755                },
2756                index=range(len(responseJSONs)),
2757                columns=["date", "time", "open", "high", "low", "close", "volume"],
2758            )
2759            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2760            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2761
2762            # append only newest candles to old history if --only-missing key present:
2763            if onlyMissing and tempOld is not None and lastTime is not None:
2764                index = 0  # find start index in tempHistory data:
2765
2766                for i, item in tempHistory.iterrows():
2767                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2768
2769                    if curTime == lastTime:
2770                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2771                        index = i
2772                        printCount = index + 1
2773                        break
2774
2775                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2776
2777            else:
2778                history = tempHistory  # if no `--only-missing` key then load full data from server
2779
2780            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2781
2782        if history is not None and not history.empty:
2783            if show:
2784                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2785                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2786                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2787                ))
2788
2789        else:
2790            uLogger.warning("Received an empty candles history!")
2791
2792        if self.historyFile is not None:
2793            if history is not None and not history.empty:
2794                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2795                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2796
2797            else:
2798                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2799
2800        else:
2801            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2802
2803        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in GetDatesAsString() method.
  • end: see docstring in GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2805    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2806        """
2807        Load candles history from csv-file and return Pandas DataFrame object.
2808
2809        See also: `History()` and `ShowHistoryChart()` methods.
2810
2811        :param filePath: path to csv-file to open.
2812        """
2813        loadedHistory = None  # init candles data object
2814
2815        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2816
2817        if os.path.exists(filePath):
2818            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2819
2820            tfStr = self.priceModel.FormattedDelta(
2821                self.priceModel.timeframe,
2822                "{days} days {hours}h {minutes}m {seconds}s",
2823            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2824                self.priceModel.timeframe,
2825                "{hours}h {minutes}m {seconds}s",
2826            )
2827
2828            if loadedHistory is not None and not loadedHistory.empty:
2829                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2830                    len(loadedHistory),
2831                    tfStr,
2832                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2833                )
2834
2835            else:
2836                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2837
2838        else:
2839            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2840
2841        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2843    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2844        """
2845        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2846
2847        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2848        Default: `index.html` (both for interact and non-interact candlesticks chart).
2849
2850        See also: `History()` and `LoadHistory()` methods.
2851
2852        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2853        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2854                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2855                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2856                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2857        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2858                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2859        """
2860        if isinstance(candles, str):
2861            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2862            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2863
2864        elif isinstance(candles, pd.DataFrame):
2865            self.priceModel.prices = candles  # set candles chain from variable
2866            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2867
2868            if "datetime" not in candles.columns:
2869                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2870
2871        else:
2872            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2873            raise Exception("Incorrect value")
2874
2875        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2876
2877        if interact:
2878            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2879
2880            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2881
2882        else:
2883            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2884
2885            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2886
2887        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2889    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2890        """
2891        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2892        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2893
2894        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2895
2896        :param operation: string "Buy" or "Sell".
2897        :param lots: volume, integer count of lots >= 1.
2898        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2899        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2900        :param expDate: string "Undefined" by default or local date in future,
2901                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2902        :return: JSON with response from broker server.
2903        """
2904        if self.accountId is None or not self.accountId:
2905            uLogger.error("Variable `accountId` must be defined for using this method!")
2906            raise Exception("Account ID required")
2907
2908        if operation is None or not operation or operation not in ("Buy", "Sell"):
2909            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2910            raise Exception("Incorrect value")
2911
2912        if lots is None or lots < 1:
2913            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2914            lots = 1
2915
2916        if tp is None or tp < 0:
2917            tp = 0
2918
2919        if sl is None or sl < 0:
2920            sl = 0
2921
2922        if expDate is None or not expDate:
2923            expDate = "Undefined"
2924
2925        if not (self.ticker or self.figi):
2926            uLogger.error("Ticker or FIGI must be defined!")
2927            raise Exception("Ticker or FIGI required")
2928
2929        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2930        self.ticker = instrument["ticker"]
2931        self.figi = instrument["figi"]
2932
2933        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2934
2935        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2936        self.body = str({
2937            "figi": self.figi,
2938            "quantity": str(lots),
2939            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2940            "accountId": str(self.accountId),
2941            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2942        })
2943        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2944
2945        if "orderId" in response.keys():
2946            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2947                operation, response["orderId"],
2948                self.ticker, self.figi, lots,
2949                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2950                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2951                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2952            ))
2953
2954            if tp > 0:
2955                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2956
2957            if sl > 0:
2958                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2959
2960        else:
2961            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2962
2963        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2965    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2966        """
2967        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2968        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2969
2970        See also: `Order()` and `Trade()` docstrings.
2971
2972        :param lots: volume, integer count of lots >= 1.
2973        :param tp: float > 0, take profit price of stop-order.
2974        :param sl: float > 0, stop loss price of stop-order.
2975        :param expDate: it's a local date in future.
2976                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2977        :return: JSON with response from broker server.
2978        """
2979        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2981    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2982        """
2983        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2984        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2985
2986        See also: `Order()` and `Trade()` docstrings.
2987
2988        :param lots: volume, integer count of lots >= 1.
2989        :param tp: float > 0, take profit price of stop-order.
2990        :param sl: float > 0, stop loss price of stop-order.
2991        :param expDate: it's a local date in the future.
2992                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2993        :return: JSON with response from broker server.
2994        """
2995        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2997    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2998        """
2999        Close position of given instruments.
3000
3001        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3002        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3003                         This avoids unnecessary downloading data from the server.
3004        """
3005        if instruments is None or not instruments:
3006            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3007            raise Exception("Ticker or FIGI required")
3008
3009        if isinstance(instruments, str):
3010            instruments = [instruments]
3011
3012        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3013        if uniqueInstruments:
3014            if portfolio is None or not portfolio:
3015                portfolio = self.Overview(show=False)
3016
3017            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3018            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3019
3020            for self.figi in uniqueInstruments:
3021                if self.figi not in allOpened:
3022                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3023                    continue
3024
3025                # search open trade info about instrument by ticker:
3026                instrument = {}
3027                for iType in TKS_INSTRUMENTS:
3028                    if instrument:
3029                        break
3030
3031                    for item in portfolio["stat"][iType]:
3032                        if item["figi"] == self.figi:
3033                            instrument = item
3034                            break
3035
3036                if instrument:
3037                    self.ticker = instrument["ticker"]
3038                    self.figi = instrument["figi"]
3039
3040                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3041                        self.ticker,
3042                        self.figi,
3043                        int(instrument["volume"]),
3044                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3045                    ))
3046
3047                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3048
3049                    if tradeLots > 0:
3050                        if instrument["blocked"] > 0:
3051                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3052                                instrument["blocked"],
3053                                self.ticker,
3054                                tradeLots,
3055                            ))
3056
3057                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3058                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3059
3060                    else:
3061                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3063    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3064        """
3065        Close all positions of given instruments with defined type.
3066
3067        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3068        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3069                         This avoids unnecessary downloading data from the server.
3070        """
3071        if iType not in TKS_INSTRUMENTS:
3072            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3073
3074        else:
3075            if portfolio is None or not portfolio:
3076                portfolio = self.Overview(show=False)
3077
3078            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3079            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3080
3081            if tickers and portfolio:
3082                self.CloseTrades(tickers, portfolio)
3083
3084            else:
3085                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3087    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3088        """
3089        Universal method to create market or limit orders with all available parameters for current `accountId`.
3090        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3091
3092        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3093        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3094
3095        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3096        then broker immediately open market order as you can do simple --buy or --sell operations!
3097
3098        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3099        When current price will go up or down to target price value then broker opens a limit order.
3100        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3101
3102        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3103
3104        :param operation: string "Buy" or "Sell".
3105        :param orderType: string "Limit" or "Stop".
3106        :param lots: volume, integer count of lots >= 1.
3107        :param targetPrice: target price > 0. This is open trade price for limit order.
3108        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3109                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3110        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3111                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3112                         Stop loss order always executed by market price.
3113        :param expDate: string "Undefined" by default or local date in future.
3114                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3115                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3116                        A limit order has no expiration date, it lasts until the end of the trading day.
3117        :return: JSON with response from broker server.
3118        """
3119        if self.accountId is None or not self.accountId:
3120            uLogger.error("Variable `accountId` must be defined for using this method!")
3121            raise Exception("Account ID required")
3122
3123        if operation is None or not operation or operation not in ("Buy", "Sell"):
3124            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3125            raise Exception("Incorrect value")
3126
3127        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3128            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3129            raise Exception("Incorrect value")
3130
3131        if lots is None or lots < 1:
3132            uLogger.error("You must define trade volume > 0: integer count of lots!")
3133            raise Exception("Incorrect value")
3134
3135        if targetPrice is None or targetPrice <= 0:
3136            uLogger.error("Target price for limit-order must be greater than 0!")
3137            raise Exception("Incorrect value")
3138
3139        if limitPrice is None or limitPrice <= 0:
3140            limitPrice = targetPrice
3141
3142        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3143            stopType = "Limit"
3144
3145        if expDate is None or not expDate:
3146            expDate = "Undefined"
3147
3148        if not (self.ticker or self.figi):
3149            uLogger.error("Tocker or FIGI must be defined!")
3150            raise Exception("Ticker or FIGI required")
3151
3152        response = {}
3153        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3154        self.ticker = instrument["ticker"]
3155        self.figi = instrument["figi"]
3156
3157        if orderType == "Limit":
3158            uLogger.debug(
3159                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3160                    self.ticker, self.figi,
3161                    operation, lots, targetPrice, instrument["currency"],
3162                ))
3163
3164            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3165            self.body = str({
3166                "figi": self.figi,
3167                "quantity": str(lots),
3168                "price": FloatToNano(targetPrice),
3169                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3170                "accountId": str(self.accountId),
3171                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3172            })
3173            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3174
3175            if "orderId" in response.keys():
3176                uLogger.info(
3177                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3178                        response["orderId"],
3179                        self.ticker, self.figi,
3180                        operation, lots, targetPrice, instrument["currency"],
3181                    ))
3182
3183                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3184                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3185                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3186                            targetPrice, instrument["currency"],
3187                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3188                        ))
3189
3190                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3191                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3192                            targetPrice, instrument["currency"],
3193                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3194                        ))
3195
3196            else:
3197                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3198
3199        if orderType == "Stop":
3200            uLogger.debug(
3201                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3202                    self.ticker, self.figi,
3203                    operation, lots,
3204                    targetPrice, instrument["currency"],
3205                    limitPrice, instrument["currency"],
3206                    stopType, expDate,
3207                ))
3208
3209            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3210            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3211            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3212
3213            body = {
3214                "figi": self.figi,
3215                "quantity": str(lots),
3216                "price": FloatToNano(limitPrice),
3217                "stopPrice": FloatToNano(targetPrice),
3218                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3219                "accountId": str(self.accountId),
3220                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3221                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3222            }
3223
3224            if expDateUTC:
3225                body["expireDate"] = expDateUTC
3226
3227            self.body = str(body)
3228            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3229
3230            if "stopOrderId" in response.keys():
3231                uLogger.info(
3232                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3233                        response["stopOrderId"],
3234                        self.ticker, self.figi,
3235                        operation, lots,
3236                        targetPrice, instrument["currency"],
3237                        limitPrice, instrument["currency"],
3238                        TKS_STOP_ORDER_TYPES[stopOrderType],
3239                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3240                    ))
3241
3242                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3243                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3244                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3245                            targetPrice, instrument["currency"],
3246                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3247                        ))
3248
3249                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3250                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3251                            targetPrice, instrument["currency"],
3252                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3253                        ))
3254
3255            else:
3256                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3257
3258        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3260    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3261        """
3262        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3263        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3264        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3265        See also: `Order()` docstring.
3266
3267        :param lots: volume, integer count of lots >= 1.
3268        :param targetPrice: target price > 0. This is open trade price for limit order.
3269        :return: JSON with response from broker server.
3270        """
3271        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3273    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3274        """
3275        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3276        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3277        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3278        target price value then broker opens a limit order. See also: `Order()` docstring.
3279
3280        :param lots: volume, integer count of lots >= 1.
3281        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3282        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3283                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3284        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3285                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3286        :param expDate: string "Undefined" by default or local date in future.
3287                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3288                        This date is converting to UTC format for server.
3289        :return: JSON with response from broker server.
3290        """
3291        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3293    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3294        """
3295        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3296        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3297        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3298        See also: `Order()` docstring.
3299
3300        :param lots: volume, integer count of lots >= 1.
3301        :param targetPrice: target price > 0. This is open trade price for limit order.
3302        :return: JSON with response from broker server.
3303        """
3304        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3306    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3307        """
3308        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3309        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3310        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3311        target price value then broker opens a limit order. See also: `Order()` docstring.
3312
3313        :param lots: volume, integer count of lots >= 1.
3314        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3315        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3316                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3317        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3318                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3319        :param expDate: string "Undefined" by default or local date in future.
3320                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3321                        This date is converting to UTC format for server.
3322        :return: JSON with response from broker server.
3323        """
3324        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3326    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3327        """
3328        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3329
3330        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3331        :param allOrdersIDs: pre-received lists of all active pending orders.
3332                             This avoids unnecessary downloading data from the server.
3333        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3334        """
3335        if self.accountId is None or not self.accountId:
3336            uLogger.error("Variable `accountId` must be defined for using this method!")
3337            raise Exception("Account ID required")
3338
3339        if orderIDs:
3340            if allOrdersIDs is None or not allOrdersIDs:
3341                rawOrders = self.RequestPendingOrders()
3342                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3343
3344            if allStopOrdersIDs is None or not allStopOrdersIDs:
3345                rawStopOrders = self.RequestStopOrders()
3346                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3347
3348            for orderID in orderIDs:
3349                idInPendingOrders = orderID in allOrdersIDs
3350                idInStopOrders = orderID in allStopOrdersIDs
3351
3352                if not (idInPendingOrders or idInStopOrders):
3353                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3354                    continue
3355
3356                else:
3357                    if idInPendingOrders:
3358                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3359
3360                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3361                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3362                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3363                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3364
3365                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3366                            if self.moreDebug:
3367                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3368
3369                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3370
3371                        else:
3372                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3373
3374                    elif idInStopOrders:
3375                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3376
3377                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3378                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3379                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3380                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3381
3382                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3383                            if self.moreDebug:
3384                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3385
3386                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3387
3388                        else:
3389                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3390
3391                    else:
3392                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3394    def CloseAllOrders(self) -> None:
3395        """
3396        Gets a list of open pending and stop orders and cancel it all.
3397        """
3398        rawOrders = self.RequestPendingOrders()
3399        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3400        lenOrders = len(allOrdersIDs)
3401
3402        rawStopOrders = self.RequestStopOrders()
3403        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3404        lenSOrders = len(allStopOrdersIDs)
3405
3406        if lenOrders > 0 or lenSOrders > 0:
3407            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3408
3409            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3410
3411        else:
3412            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3414    def CloseAll(self, *args) -> None:
3415        """
3416        Close all available (not blocked) opened trades and orders.
3417
3418        Also, you can select one or more keywords case-insensitive:
3419        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3420
3421        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3422        """
3423        overview = self.Overview(show=False)  # get all open trades info
3424
3425        if len(args) == 0:
3426            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3427            self.CloseAllOrders()  # close all pending and stop orders
3428
3429            for iType in TKS_INSTRUMENTS:
3430                if iType != "Currencies":
3431                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3432
3433        else:
3434            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3435            lowerArgs = [x.lower() for x in args]
3436
3437            if "orders" in lowerArgs:
3438                self.CloseAllOrders()  # close all pending and stop orders
3439
3440            for iType in TKS_INSTRUMENTS:
3441                if iType.lower() in lowerArgs and iType != "Currencies":
3442                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3444    @staticmethod
3445    def ParseOrderParameters(operation, **inputParameters):
3446        """
3447        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3448
3449        :param operation: string "Buy" or "Sell".
3450        :param inputParameters: this is dict of strings that looks like this
3451               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3452               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3453               "prices" key: one or more prices to open limit-orders
3454               Counts of values in lots and prices lists must be equals!
3455        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3456        """
3457        # TODO: update order grid work with api v2
3458        pass
3459        # uLogger.debug("Input parameters: {}".format(inputParameters))
3460        #
3461        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3462        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3463        #     raise Exception("Incorrect value")
3464        #
3465        # if "l" in inputParameters.keys():
3466        #     inputParameters["lots"] = inputParameters.pop("l")
3467        #
3468        # if "p" in inputParameters.keys():
3469        #     inputParameters["prices"] = inputParameters.pop("p")
3470        #
3471        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3472        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3473        #     raise Exception("Incorrect value")
3474        #
3475        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3476        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3477        #
3478        # if len(lots) != len(prices):
3479        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3480        #     raise Exception("Incorrect value")
3481        #
3482        # uLogger.debug("Extracted parameters for orders:")
3483        # uLogger.debug("lots = {}".format(lots))
3484        # uLogger.debug("prices = {}".format(prices))
3485        #
3486        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3487        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3488        # uLogger.debug("Order parameters: {}".format(result))
3489        #
3490        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3492    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3493        """
3494        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3495
3496        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3497        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3498        """
3499        result = False
3500        msg = "Instrument not defined!"
3501
3502        if portfolio is None or not portfolio:
3503            portfolio = self.Overview(show=False)
3504
3505        if self.ticker:
3506            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3507            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3508
3509            for iType in TKS_INSTRUMENTS:
3510                for instrument in portfolio["stat"][iType]:
3511                    if instrument["ticker"] == self.ticker:
3512                        result = True
3513                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3514                        break
3515
3516        elif self.figi:
3517            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3518            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3519
3520            for iType in TKS_INSTRUMENTS:
3521                for instrument in portfolio["stat"][iType]:
3522                    if instrument["figi"] == self.figi:
3523                        result = True
3524                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3525                        break
3526
3527        else:
3528            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3529
3530        uLogger.debug(msg)
3531
3532        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3534    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3535        """
3536        Returns instrument from the user's portfolio if it presents there.
3537        Instrument must be defined by `ticker` (highly priority) or `figi`.
3538
3539        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3540        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3541        """
3542        result = None
3543        msg = "Instrument not defined!"
3544
3545        if portfolio is None or not portfolio:
3546            portfolio = self.Overview(show=False)
3547
3548        if self.ticker:
3549            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3550            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3551
3552            for iType in TKS_INSTRUMENTS:
3553                for instrument in portfolio["stat"][iType]:
3554                    if instrument["ticker"] == self.ticker:
3555                        result = instrument
3556                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3557                        break
3558
3559        elif self.figi:
3560            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3561            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3562
3563            for iType in TKS_INSTRUMENTS:
3564                for instrument in portfolio["stat"][iType]:
3565                    if instrument["figi"] == self.figi:
3566                        result = instrument
3567                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3568                        break
3569
3570        else:
3571            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3572
3573        uLogger.debug(msg)
3574
3575        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def RequestLimits(self) -> dict:
3577    def RequestLimits(self) -> dict:
3578        """
3579        Method for obtaining the available funds for withdrawal for current `accountId`.
3580
3581        See also:
3582        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3583        - `OverviewLimits()` method
3584
3585        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3586                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3587                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3588                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3589        """
3590        if self.accountId is None or not self.accountId:
3591            uLogger.error("Variable `accountId` must be defined for using this method!")
3592            raise Exception("Account ID required")
3593
3594        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3595
3596        self.body = str({"accountId": self.accountId})
3597        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3598        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3599
3600        if self.moreDebug:
3601            uLogger.debug("Records about available funds for withdrawal successfully received")
3602
3603        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3605    def OverviewLimits(self, show: bool = False) -> dict:
3606        """
3607        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3608
3609        See also: `RequestLimits()`.
3610
3611        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3612        :return: dict with raw parsed data from server and some calculated statistics about it.
3613        """
3614        if self.accountId is None or not self.accountId:
3615            uLogger.error("Variable `accountId` must be defined for using this method!")
3616            raise Exception("Account ID required")
3617
3618        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3619
3620        view = {
3621            "rawLimits": rawLimits,
3622            "limits": {  # parsed data for every currency:
3623                "money": {  # this is an array of portfolio currency positions
3624                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3625                },
3626                "blocked": {  # this is an array of blocked currency
3627                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3628                },
3629                "blockedGuarantee": {  # this is locked money under collateral for futures
3630                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3631                },
3632            },
3633        }
3634
3635        # --- Prepare text table with limits in human-readable format:
3636        if show:
3637            info = [
3638                "# Withdrawal limits\n\n",
3639                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3640                "* **Account ID:** [{}]\n".format(self.accountId),
3641            ]
3642
3643            if view["limits"]["money"]:
3644                info.extend([
3645                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3646                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3647                ])
3648
3649            else:
3650                info.append("\nNo withdrawal limits\n")
3651
3652            for curr in view["limits"]["money"].keys():
3653                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3654                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3655                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3656
3657                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3658                    "[{}]".format(curr),
3659                    "{:.2f}".format(view["limits"]["money"][curr]),
3660                    "{:.2f}".format(availableMoney),
3661                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3662                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3663                )
3664
3665                if curr == "rub":
3666                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3667
3668                else:
3669                    info.append(infoStr)
3670
3671            infoText = "".join(info)
3672
3673            uLogger.info(infoText)
3674
3675            if self.withdrawalLimitsFile:
3676                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3677                    fH.write(infoText)
3678
3679                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3680
3681        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3683    def RequestAccounts(self) -> dict:
3684        """
3685        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3686
3687        See also:
3688        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3689        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3690        - `OverviewUserInfo()` method
3691
3692        :return: dict with raw data from server that contains accounts info. Example of dict:
3693                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3694                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3695                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3696                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3697        """
3698        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3699
3700        self.body = str({})
3701        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3702        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3703
3704        if self.moreDebug:
3705            uLogger.debug("Records about available accounts successfully received")
3706
3707        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3709    def RequestUserInfo(self) -> dict:
3710        """
3711        Method for requesting common user's information.
3712
3713        See also:
3714        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3715        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3716        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3717        - `OverviewUserInfo()` method
3718
3719        :return: dict with raw data from server that contains user's information. Example of dict:
3720                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3721                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3722        """
3723        uLogger.debug("Requesting common user's information. Wait, please...")
3724
3725        self.body = str({})
3726        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3727        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3728
3729        if self.moreDebug:
3730            uLogger.debug("Records about current user successfully received")
3731
3732        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3734    def RequestMarginStatus(self, accountId: str = None) -> dict:
3735        """
3736        Method for requesting margin calculation for defined account ID.
3737
3738        See also:
3739        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3740        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3741        - `OverviewUserInfo()` method
3742
3743        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3744        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3745                 Example of responses:
3746                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3747                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3748                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3749                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3750                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3751                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3752        """
3753        if accountId is None or not accountId:
3754            if self.accountId is None or not self.accountId:
3755                uLogger.error("Variable `accountId` must be defined for using this method!")
3756                raise Exception("Account ID required")
3757
3758            else:
3759                accountId = self.accountId  # use `self.accountId` (main ID) by default
3760
3761        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3762
3763        self.body = str({"accountId": accountId})
3764        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3765        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3766
3767        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3768            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3769            rawMargin = {}
3770
3771        else:
3772            if self.moreDebug:
3773                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3774
3775        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
3777    def RequestTariffLimits(self) -> dict:
3778        """
3779        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3780
3781        See also:
3782        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3783        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3784        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3785        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3786        - `OverviewUserInfo()` method
3787
3788        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3789                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3790                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3791        """
3792        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3793
3794        self.body = str({})
3795        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3796        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3797
3798        if self.moreDebug:
3799            uLogger.debug("Records with limits of current tariff successfully received")
3800
3801        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
3803    def RequestBondCoupons(self, iJSON: dict) -> dict:
3804        """
3805        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3806        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3807        All dates are in UTC timezone.
3808
3809        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3810        Documentation:
3811        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3812        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3813
3814        See also: `ExtendBondsData()`.
3815
3816        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3817                      If raw iJSON is not data of bond then server returns an error [400] with message:
3818                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3819        :return: dictionary with bond payment calendar. Response example
3820                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3821                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3822                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3823                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3824        """
3825        if iJSON["figi"] is None or not iJSON["figi"]:
3826            uLogger.error("FIGI must be defined for using this method!")
3827            raise Exception("FIGI required")
3828
3829        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3830        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3831
3832        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3833            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3834            self.figi,
3835            startDate,
3836            endDate,
3837        ))
3838
3839        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3840        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3841        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3842
3843        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3844            uLogger.warning("Instrument type is not bond!")
3845
3846        else:
3847            if self.moreDebug:
3848                uLogger.debug("Records about bond payment calendar successfully received")
3849
3850        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self.ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
3852    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3853        """
3854        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3855        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3856        coupon yields, current yields and some statistics etc.
3857
3858        WARNING! This is too long operation if a lot of bonds requested from broker server.
3859
3860        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3861
3862        :param instruments: list of strings with tickers or FIGIs.
3863        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3864                     for further used by data scientists or stock analytics.
3865        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3866                 In XLSX-file and Pandas DataFrame fields mean:
3867                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3868                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3869        """
3870        if instruments is None or not instruments:
3871            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3872            raise Exception("Ticker or FIGI required")
3873
3874        if isinstance(instruments, str):
3875            instruments = [instruments]
3876
3877        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3878
3879        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3880
3881        iCount = len(uniqueInstruments)
3882        tooLong = iCount >= 20
3883        if tooLong:
3884            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3885
3886        bonds = None
3887        for i, self.figi in enumerate(uniqueInstruments):
3888            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3889
3890            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3891                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3892                rawBond = self.SearchByFIGI(requestPrice=True)
3893
3894                # Widen raw data with UTC current time (iData["actualDateTime"]):
3895                actualDate = datetime.now(tzutc())
3896                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3897
3898                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3899                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3900
3901                # Replace some values with human-readable:
3902                iData["nominalCurrency"] = iData["nominal"]["currency"]
3903                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3904                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3905                iData["aciCurrency"] = iData["aciValue"]["currency"]
3906                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3907                iData["issueSize"] = int(iData["issueSize"])
3908                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3909                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3910                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3911                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3912                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3913                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3914                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3915                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3916                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3917                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3918
3919                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3920                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3921                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3922                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3923                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3924                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3925                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3926                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3927                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3928                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3929                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3930
3931                # Widen raw data with calendar data from `rawCalendar` values:
3932                calendarData = []
3933                if "events" in iData["rawCalendar"].keys():
3934                    for item in iData["rawCalendar"]["events"]:
3935                        calendarData.append({
3936                            "couponDate": item["couponDate"],
3937                            "couponNumber": int(item["couponNumber"]),
3938                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3939                            "payCurrency": item["payOneBond"]["currency"],
3940                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3941                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3942                            "couponStartDate": item["couponStartDate"],
3943                            "couponEndDate": item["couponEndDate"],
3944                            "couponPeriod": item["couponPeriod"],
3945                        })
3946
3947                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3948                    if "maturityDate" not in iData.keys():
3949                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3950
3951                # Widen raw data with Coupon Rate.
3952                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3953                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3954                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3955                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3956
3957                # Widen raw data with Yield to Maturity (YTM) on current date.
3958                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3959                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3960                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3961                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3962                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3963                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3964
3965                iData["calendar"] = calendarData  # adds calendar at the end
3966
3967                # Remove not used data:
3968                iData.pop("uid")
3969                iData.pop("positionUid")
3970                iData.pop("currentPrice")
3971                iData.pop("rawCalendar")
3972
3973                colNames = list(iData.keys())
3974                if bonds is None:
3975                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3976
3977                else:
3978                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3979
3980            else:
3981                uLogger.warning("Instrument is not a bond!")
3982
3983            processed = round(100 * (i + 1) / iCount, 1)
3984            if tooLong and processed % 5 == 0:
3985                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3986
3987            else:
3988                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3989
3990        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3991
3992        # Saving bonds from Pandas DataFrame to XLSX sheet:
3993        if xlsx and self.bondsXLSXFile:
3994            with pd.ExcelWriter(
3995                    path=self.bondsXLSXFile,
3996                    date_format=TKS_DATE_FORMAT,
3997                    datetime_format=TKS_DATE_TIME_FORMAT,
3998                    mode="w",
3999            ) as writer:
4000                bonds.to_excel(
4001                    writer,
4002                    sheet_name="Extended bonds data",
4003                    index=True,
4004                    encoding="UTF-8",
4005                    freeze_panes=(1, 1),
4006                )  # saving as XLSX-file with freeze first row and column as headers
4007
4008            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4009
4010        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4012    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4013        """
4014        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4015
4016        WARNING! This is too long operation if a lot of bonds requested from broker server.
4017
4018        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4019
4020        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4021                        extended information about bonds: main info, current prices, bond payment calendar,
4022                        coupon yields, current yields and some statistics etc.
4023                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4024        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4025                     for further used by data scientists or stock analytics.
4026        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4027        """
4028        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4029            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4030
4031        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4032
4033        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4034        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4035        calendar = None
4036        for bond in extBonds.iterrows():
4037            for item in bond[1]["calendar"]:
4038                cData = {
4039                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4040                    "couponDate": item["couponDate"],
4041                    "figi": bond[1]["figi"],
4042                    "ticker": bond[1]["ticker"],
4043                    "name": bond[1]["name"],
4044                    "couponNumber": item["couponNumber"],
4045                    "payOneBond": item["payOneBond"],
4046                    "payCurrency": item["payCurrency"],
4047                    "couponType": item["couponType"],
4048                    "couponPeriod": item["couponPeriod"],
4049                    "fixDate": item["fixDate"],
4050                    "couponStartDate": item["couponStartDate"],
4051                    "couponEndDate": item["couponEndDate"],
4052                }
4053
4054                if calendar is None:
4055                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4056
4057                else:
4058                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4059
4060        if calendar is not None:
4061            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4062
4063            # Saving calendar from Pandas DataFrame to XLSX sheet:
4064            if xlsx:
4065                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4066
4067                with pd.ExcelWriter(
4068                        path=xlsxCalendarFile,
4069                        date_format=TKS_DATE_FORMAT,
4070                        datetime_format=TKS_DATE_TIME_FORMAT,
4071                        mode="w",
4072                ) as writer:
4073                    humanReadable = calendar.copy(deep=True)
4074                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4075                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4076                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4077                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4078                    humanReadable.columns = colNames  # human-readable column names
4079
4080                    humanReadable.to_excel(
4081                        writer,
4082                        sheet_name="Bond payments calendar",
4083                        index=False,
4084                        encoding="UTF-8",
4085                        freeze_panes=(1, 2),
4086                    )  # saving as XLSX-file with freeze first row and column as headers
4087
4088                    del humanReadable  # release df in memory
4089
4090                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4091
4092        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4094    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4095        """
4096        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4097        Also, creates Markdown file with calendar data, `calendar.md` by default.
4098
4099        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4100
4101        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4102                        extended information about bonds: main info, current prices, bond payment calendar,
4103                        coupon yields, current yields and some statistics etc.
4104                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4105        :param show: if `True` then also printing bonds payment calendar to the console,
4106                     otherwise save to file `calendarFile` only. `False` by default.
4107        :return: multilines text in Markdown format with bonds payment calendar as a table.
4108        """
4109        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4110            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4111
4112        infoText = "# Bond payments calendar\n\n"
4113
4114        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4115
4116        if not (calendar is None or calendar.empty):
4117            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4118
4119            info = [
4120                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4121                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4122            ]
4123
4124            newMonth = False
4125            notOneBond = calendar["figi"].nunique() > 1
4126            for i, bond in enumerate(calendar.iterrows()):
4127                if newMonth and notOneBond:
4128                    info.append(splitLine)
4129
4130                info.append(
4131                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4132                        "  √" if bond[1]["paid"] else "  —",
4133                        bond[1]["couponDate"].split("T")[0],
4134                        bond[1]["figi"],
4135                        bond[1]["ticker"],
4136                        bond[1]["couponNumber"],
4137                        "{} {}".format(
4138                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4139                            bond[1]["payCurrency"],
4140                        ),
4141                        bond[1]["couponType"],
4142                        bond[1]["couponPeriod"],
4143                        bond[1]["fixDate"].split("T")[0],
4144                    )
4145                )
4146
4147                if i < len(calendar.values) - 1:
4148                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4149                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4150                    newMonth = False if curDate.month == nextDate.month else True
4151
4152                else:
4153                    newMonth = False
4154
4155            infoText += "".join(info)
4156
4157            if show:
4158                uLogger.info("{}".format(infoText))
4159
4160            if self.calendarFile is not None:
4161                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4162                    fH.write(infoText)
4163
4164                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4165
4166        else:
4167            infoText += "No data\n"
4168
4169        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4171    def OverviewAccounts(self, show: bool = False) -> dict:
4172        """
4173        Method for parsing and show simple table with all available user accounts.
4174
4175        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4176
4177        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4178        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4179                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4180                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4181                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4182                                                        "closed": "—", "access": "Full access" }, ...}}`
4183        """
4184        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4185
4186        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4187        accounts = {
4188            item["id"]: {
4189                "type": TKS_ACCOUNT_TYPES[item["type"]],
4190                "name": item["name"],
4191                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4192                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4193                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4194                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4195            } for item in rawAccounts["accounts"]
4196        }
4197
4198        # Raw and parsed data with some fields replaced in "stat" section:
4199        view = {
4200            "rawAccounts": rawAccounts,
4201            "stat": accounts,
4202        }
4203
4204        # --- Prepare simple text table with only accounts data in human-readable format:
4205        if show:
4206            info = [
4207                "# User accounts\n\n",
4208                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4209                "| Account ID   | Type                      | Status                    | Name                           |\n",
4210                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4211            ]
4212
4213            for account in view["stat"].keys():
4214                info.extend([
4215                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4216                        account,
4217                        view["stat"][account]["type"],
4218                        view["stat"][account]["status"],
4219                        view["stat"][account]["name"],
4220                    )
4221                ])
4222
4223            infoText = "".join(info)
4224
4225            uLogger.info(infoText)
4226
4227            if self.userAccountsFile:
4228                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4229                    fH.write(infoText)
4230
4231                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4232
4233        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4235    def OverviewUserInfo(self, show: bool = False) -> dict:
4236        """
4237        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4238
4239        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4240
4241        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4242        :return: dict with raw parsed data from server and some calculated statistics about it.
4243        """
4244        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4245        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4246        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4247        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4248        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4249        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4250
4251        # This is dict with parsed common user data:
4252        userInfo = {
4253            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4254            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4255            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4256            "tariff": rawUserInfo["tariff"],
4257        }
4258
4259        # This is an array of dict with parsed margin statuses for every account IDs:
4260        margins = {}
4261        for accountId in accounts.keys():
4262            if rawMargins[accountId]:
4263                margins[accountId] = {
4264                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4265                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4266                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4267                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4268                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4269                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4270                }
4271
4272            else:
4273                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4274
4275        unary = {}  # unary-connection limits
4276        for item in rawTariffLimits["unaryLimits"]:
4277            if item["limitPerMinute"] in unary.keys():
4278                unary[item["limitPerMinute"]].extend(item["methods"])
4279
4280            else:
4281                unary[item["limitPerMinute"]] = item["methods"]
4282
4283        stream = {}  # stream-connection limits
4284        for item in rawTariffLimits["streamLimits"]:
4285            if item["limit"] in stream.keys():
4286                stream[item["limit"]].extend(item["streams"])
4287
4288            else:
4289                stream[item["limit"]] = item["streams"]
4290
4291        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4292        limits = {
4293            "unary": unary,
4294            "stream": stream,
4295        }
4296
4297        # Raw and parsed data as an output result:
4298        view = {
4299            "rawUserInfo": rawUserInfo,
4300            "rawAccounts": rawAccounts,
4301            "rawMargins": rawMargins,
4302            "rawTariffLimits": rawTariffLimits,
4303            "stat": {
4304                "userInfo": userInfo,
4305                "accounts": accounts,
4306                "margins": margins,
4307                "limits": limits,
4308            },
4309        }
4310
4311        # --- Prepare text table with user information in human-readable format:
4312        if show:
4313            info = [
4314                "# Full user information\n\n",
4315                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4316                "## Common information\n\n",
4317                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4318                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4319                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4320                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4321                "\n## User accounts\n\n",
4322            ]
4323
4324            for account in view["stat"]["accounts"].keys():
4325                info.extend([
4326                    "### ID: [{}]\n\n".format(account),
4327                    "| Parameters           | Values                                                       |\n",
4328                    "|----------------------|--------------------------------------------------------------|\n",
4329                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4330                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4331                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4332                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4333                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4334                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4335                ])
4336
4337                if margins[account]:
4338                    info.extend([
4339                        "| Margin status:       | Enabled                                                      |\n",
4340                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4341                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4342                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4343                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4344                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4345                    ])
4346
4347                else:
4348                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4349
4350            info.extend([
4351                "\n## Current user tariff limits\n",
4352                "\nSee also:\n",
4353                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4354                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4355                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4356                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4357                "\n### Unary limits\n",
4358            ])
4359
4360            if unary:
4361                for key, values in sorted(unary.items()):
4362                    info.append("\n* Max requests per minute: {}\n".format(key))
4363
4364                    for value in values:
4365                        info.append("  - {}\n".format(value))
4366
4367            else:
4368                info.append("\nNot available\n")
4369
4370            info.append("\n### Stream limits\n")
4371
4372            if stream:
4373                for key, values in sorted(stream.items()):
4374                    info.append("\n* Max stream connections: {}\n".format(key))
4375
4376                    for value in values:
4377                        info.append("  - {}\n".format(value))
4378
4379            else:
4380                info.append("\nNot available\n")
4381
4382            infoText = "".join(info)
4383
4384            uLogger.info(infoText)
4385
4386            if self.userInfoFile:
4387                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4388                    fH.write(infoText)
4389
4390                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4391
4392        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4395class Args:
4396    """
4397    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4398    """
4399    def __init__(self, **kwargs):
4400        self.__dict__.update(kwargs)
4401
4402    def __getattr__(self, item):
4403        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4399    def __init__(self, **kwargs):
4400        self.__dict__.update(kwargs)
def ParseArgs()
4406def ParseArgs():
4407    """This function get and parse command line keys."""
4408    parser = ArgumentParser()  # command-line string parser
4409
4410    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4411    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4412
4413    # --- options:
4414
4415    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4416    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4417    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4418
4419    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4420    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4421
4422    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4423    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4424
4425    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4426
4427    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4428    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4429    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4430
4431    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4432    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4433
4434    # --- commands:
4435
4436    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4437
4438    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4439    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4440    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4441    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4442    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4443    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4444    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4445    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4446
4447    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4448    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4449    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4450    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4451    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4452    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4453
4454    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4455    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4456    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4457    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4458
4459    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4460    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4461    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4462
4463    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4464    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4465    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4466    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4467    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4468    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4469    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4470
4471    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4472    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4473    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4474    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4475    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4476
4477    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4478    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4479    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4480
4481    cmdArgs = parser.parse_args()
4482    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs)
4485def Main(**kwargs):
4486    """
4487    Main function for work with TKSBrokerAPI in the console.
4488
4489    See examples:
4490    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4491    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4492    """
4493    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4494
4495    if args.debug_level:
4496        uLogger.level = 10  # always debug level by default
4497        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4498
4499    exitCode = 0
4500    start = datetime.now(tzutc())
4501    uLogger.debug("=-" * 50)
4502    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4503        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4504        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4505    ))
4506
4507    # trying to calculate full current version:
4508    buildVersion = __version__
4509    try:
4510        v = version("tksbrokerapi")
4511        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4512
4513    except Exception:
4514        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4515
4516    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4517    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4518
4519    try:
4520        if args.version:
4521            print("TKSBrokerAPI {}".format(buildVersion))
4522            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4523
4524        else:
4525            # Init class for trading with Tinkoff Broker:
4526            trader = TinkoffBrokerServer(
4527                token=args.token,
4528                accountId=args.account_id,
4529                useCache=not args.no_cache,
4530            )
4531
4532            # --- set some options:
4533
4534            if args.more:
4535                trader.moreDebug = True
4536                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4537
4538            if args.ticker:
4539                ticker = args.ticker.upper()  # Tickers may be upper case only
4540
4541                if ticker in trader.aliasesKeys:
4542                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4543
4544                else:
4545                    trader.ticker = ticker
4546
4547            if args.figi:
4548                trader.figi = args.figi.upper()  # FIGIs may be upper case only
4549
4550            if args.depth is not None:
4551                trader.depth = args.depth
4552
4553            # --- do one command:
4554
4555            if args.list:
4556                if args.output is not None:
4557                    trader.instrumentsFile = args.output
4558
4559                trader.ShowInstrumentsInfo(show=True)
4560
4561            elif args.list_xlsx:
4562                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4563
4564            elif args.bonds_xlsx is not None:
4565                if args.output is not None:
4566                    trader.bondsXLSXFile = args.output
4567
4568                if len(args.bonds_xlsx) == 0:
4569                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4570
4571                else:
4572                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4573
4574            elif args.search:
4575                if args.output is not None:
4576                    trader.searchResultsFile = args.output
4577
4578                trader.SearchInstruments(pattern=args.search[0], show=True)
4579
4580            elif args.info:
4581                if not (args.ticker or args.figi):
4582                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4583                    raise Exception("Ticker or FIGI required")
4584
4585                if args.output is not None:
4586                    trader.infoFile = args.output
4587
4588                if args.ticker:
4589                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4590
4591                else:
4592                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4593
4594            elif args.calendar is not None:
4595                if args.output is not None:
4596                    trader.calendarFile = args.output
4597
4598                if len(args.calendar) == 0:
4599                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4600
4601                else:
4602                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4603
4604                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4605
4606            elif args.price:
4607                if not (args.ticker or args.figi):
4608                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4609                    raise Exception("Ticker or FIGI required")
4610
4611                trader.GetCurrentPrices(show=True)
4612
4613            elif args.prices is not None:
4614                if args.output is not None:
4615                    trader.pricesFile = args.output
4616
4617                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4618
4619            elif args.overview:
4620                if args.output is not None:
4621                    trader.overviewFile = args.output
4622
4623                trader.Overview(show=True, details="full")
4624
4625            elif args.overview_digest:
4626                if args.output is not None:
4627                    trader.overviewDigestFile = args.output
4628
4629                trader.Overview(show=True, details="digest")
4630
4631            elif args.overview_positions:
4632                if args.output is not None:
4633                    trader.overviewPositionsFile = args.output
4634
4635                trader.Overview(show=True, details="positions")
4636
4637            elif args.overview_orders:
4638                if args.output is not None:
4639                    trader.overviewOrdersFile = args.output
4640
4641                trader.Overview(show=True, details="orders")
4642
4643            elif args.overview_analytics:
4644                if args.output is not None:
4645                    trader.overviewAnalyticsFile = args.output
4646
4647                trader.Overview(show=True, details="analytics")
4648
4649            elif args.overview_calendar:
4650                if args.output is not None:
4651                    trader.overviewAnalyticsFile = args.output
4652
4653                trader.Overview(show=True, details="calendar")
4654
4655            elif args.deals is not None:
4656                if args.output is not None:
4657                    trader.reportFile = args.output
4658
4659                if 0 <= len(args.deals) < 3:
4660                    trader.Deals(
4661                        start=args.deals[0] if len(args.deals) >= 1 else None,
4662                        end=args.deals[1] if len(args.deals) == 2 else None,
4663                        show=True,  # Always show deals report in console
4664                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4665                    )
4666
4667                else:
4668                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4669                    raise Exception("Incorrect value")
4670
4671            elif args.history is not None:
4672                if args.output is not None:
4673                    trader.historyFile = args.output
4674
4675                if 0 <= len(args.history) < 3:
4676                    dataReceived = trader.History(
4677                        start=args.history[0] if len(args.history) >= 1 else None,
4678                        end=args.history[1] if len(args.history) == 2 else None,
4679                        interval="hour" if args.interval is None or not args.interval else args.interval,
4680                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4681                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4682                        show=True,  # shows all downloaded candles in console
4683                    )
4684
4685                    if args.render_chart is not None and dataReceived is not None:
4686                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4687
4688                        trader.ShowHistoryChart(
4689                            candles=dataReceived,
4690                            interact=iChart,
4691                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4692                        )
4693
4694                else:
4695                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4696                    raise Exception("Incorrect value")
4697
4698            elif args.load_history is not None:
4699                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4700
4701                if args.render_chart is not None and histData is not None:
4702                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4703                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4704
4705                    trader.ShowHistoryChart(
4706                        candles=histData,
4707                        interact=iChart,
4708                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4709                    )
4710
4711            elif args.trade is not None:
4712                if 1 <= len(args.trade) <= 5:
4713                    trader.Trade(
4714                        operation=args.trade[0],
4715                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4716                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4717                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4718                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4719                    )
4720
4721                else:
4722                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4723
4724            elif args.buy is not None:
4725                if 0 <= len(args.buy) <= 4:
4726                    trader.Buy(
4727                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4728                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4729                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4730                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4731                    )
4732
4733                else:
4734                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4735
4736            elif args.sell is not None:
4737                if 0 <= len(args.sell) <= 4:
4738                    trader.Sell(
4739                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4740                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4741                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4742                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4743                    )
4744
4745                else:
4746                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4747
4748            elif args.order:
4749                if 4 <= len(args.order) <= 7:
4750                    trader.Order(
4751                        operation=args.order[0],
4752                        orderType=args.order[1],
4753                        lots=int(args.order[2]),
4754                        targetPrice=float(args.order[3]),
4755                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4756                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4757                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4758                    )
4759
4760                else:
4761                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4762
4763            elif args.buy_limit:
4764                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4765
4766            elif args.sell_limit:
4767                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4768
4769            elif args.buy_stop:
4770                if 2 <= len(args.buy_stop) <= 7:
4771                    trader.BuyStop(
4772                        lots=int(args.buy_stop[0]),
4773                        targetPrice=float(args.buy_stop[1]),
4774                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4775                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4776                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4777                    )
4778
4779                else:
4780                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4781
4782            elif args.sell_stop:
4783                if 2 <= len(args.sell_stop) <= 7:
4784                    trader.SellStop(
4785                        lots=int(args.sell_stop[0]),
4786                        targetPrice=float(args.sell_stop[1]),
4787                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4788                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4789                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4790                    )
4791
4792                else:
4793                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4794
4795            # elif args.buy_order_grid is not None:
4796            #     # update order grid work with api v2
4797            #     if len(args.buy_order_grid) == 2:
4798            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4799            #
4800            #         for order in orderParams:
4801            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4802            #
4803            #     else:
4804            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4805            #
4806            # elif args.sell_order_grid is not None:
4807            #     # update order grid work with api v2
4808            #     if len(args.sell_order_grid) >= 2:
4809            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4810            #
4811            #         for order in orderParams:
4812            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4813            #
4814            #     else:
4815            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4816
4817            elif args.close_order is not None:
4818                trader.CloseOrders(args.close_order)  # close only one order
4819
4820            elif args.close_orders is not None:
4821                trader.CloseOrders(args.close_orders)  # close list of orders
4822
4823            elif args.close_trade:
4824                if not (args.ticker or args.figi):
4825                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4826                    raise Exception("Ticker or FIGI required")
4827
4828                if args.ticker:
4829                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4830
4831                else:
4832                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
4833
4834            elif args.close_trades is not None:
4835                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
4836
4837            elif args.close_all is not None:
4838                trader.CloseAll(*args.close_all)
4839
4840            elif args.limits:
4841                if args.output is not None:
4842                    trader.withdrawalLimitsFile = args.output
4843
4844                trader.OverviewLimits(show=True)
4845
4846            elif args.user_info:
4847                if args.output is not None:
4848                    trader.userInfoFile = args.output
4849
4850                trader.OverviewUserInfo(show=True)
4851
4852            elif args.account:
4853                if args.output is not None:
4854                    trader.userAccountsFile = args.output
4855
4856                trader.OverviewAccounts(show=True)
4857
4858            else:
4859                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4860                raise Exception("There is no command to execute")
4861
4862    except Exception:
4863        trace = tb.format_exc()
4864        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4865            if e in trace:
4866                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4867                break
4868
4869        uLogger.debug(trace)
4870        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4871        exitCode = 255  # an error occurred, must be open a ticket for this issue
4872
4873    finally:
4874        finish = datetime.now(tzutc())
4875
4876        if exitCode == 0:
4877            if args.more:
4878                uLogger.debug("All operations were finished success (summary code is 0).")
4879
4880        else:
4881            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4882                os.path.abspath(uLog.defaultLogFile), exitCode,
4883            ))
4884
4885        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4886        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4887            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4888            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4889        ))
4890        uLogger.debug("=-" * 50)
4891
4892        if not kwargs:
4893            sys.exit(exitCode)
4894
4895        else:
4896            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: